Skip to content

fix(build): npm ci --ignore-scripts in the wheel build hook (GHSA-pfvh, code-only)#461

Merged
padak merged 1 commit into
mainfrom
fix/build-ignore-scripts
Jun 23, 2026
Merged

fix(build): npm ci --ignore-scripts in the wheel build hook (GHSA-pfvh, code-only)#461
padak merged 1 commit into
mainfrom
fix/build-ignore-scripts

Conversation

@padak

@padak padak commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

Fixes M9 from the 2026-06-12 security audit (private advisory GHSA-pfvh-6q5w-9m52) — an install-time RCE surface in the wheel build hook.

scripts/hatch_build.py runs npm ci to build the React SPA at wheel-build time and when a user installs via git+https://github.com/keboola/cli (the fallback when no prebuilt wheel asset is present). Without --ignore-scripts, every preinstall/install/postinstall lifecycle script in the (transitive) npm dependency tree executes on the user's machine during install — so a single compromised transitive dependency lands arbitrary code execution on every user who takes the git+ install path with Node on PATH.

Fix

Add --ignore-scripts to the npm ci invocation. tsc -b && vite build compiles the SPA without needing any dependency install-script.

Verified non-breaking (not assumed)

--ignore-scripts can break builds whose deps fetch a binary via postinstall (the classic esbuild trap). I ran the real build with the flag in web/frontend:

npm ci --ignore-scripts --prefer-offline --no-audit --no-fund   # added 393 packages, exit 0
npm run build                                                    # tsc -b && vite build -> ✓ built, dist/index.html present

vite 8 uses rolldown (Rust bundler) which ships prebuilt platform binaries as regular package files, not via a postinstall download — so --ignore-scripts is safe here. The lockfile (package-lock.json) remains committed + integrity-pinned, so this is defense-in-depth on top of the existing tarball-hash verification.

Tests

tests/test_build_hook.py::test_passes_resolved_npm_path_not_bare_name now also asserts --ignore-scripts is in the npm ci command (mock-based regression test). Full suite green: 4177 passed, 135 skipped; lint/format clean.

⚠️ Code-only PR

Touches only scripts/hatch_build.py + tests/test_build_hook.pyno version bump / changelog entry (added at the next release, same conflict-immunity rationale as #422/#460).

Audit progress

Open after this: M7 (plaintext-on-encrypt warning — 3 services), M10 (version regex), + M1/M5 residuals.


Open in Devin Review

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@padak padak left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of #461 — fix(build): npm ci --ignore-scripts in the wheel build hook (GHSA-pfvh, code-only)

Generated by kbagent-pr-reviewer subagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed via make check, not duplicated here.

Summary

This PR adds --ignore-scripts to the npm ci invocation in scripts/hatch_build.py to close a real install-time RCE surface (GHSA-pfvh-6q5w-9m52): without the flag, any postinstall/preinstall lifecycle script in the transitive npm dependency tree executes on the user's machine during git+ install — including on machines that have never consciously run npm. The fix is one-line surgical; the test assertion (assert "--ignore-scripts" in ci_cmd) is correctly scoped to the npm ci call only and does not incorrectly constrain the npm run build call. The deliberate deferral of the version bump and changelog entry is noted in the PR description and is consistent with the documented conflict-immunity pattern used in #422 and #460.

Verdict: APPROVE. No blocking findings. Two non-blocking observations and one nit follow.

Verdict

  • Verdict: APPROVE
  • Blocking findings: 0
  • Non-blocking findings: 2
  • Nits: 1

Blocking findings

(none)

Non-blocking findings

[NB-1] tests/test_build_hook.py:209 — test does not assert that --ignore-scripts is absent from the npm run build command

The assertion at line 210 (assert build_cmd[1:] == ["run", "build"]) implicitly confirms --ignore-scripts is not in the build_cmd, but it does so as a positive equality check on the full tail rather than an explicit negative assertion. This is correct and sufficient — npm run ignores the flag anyway — but a future maintainer reading the test may not realize the build_cmd shape is intentionally different from ci_cmd. A one-line comment like # --ignore-scripts intentionally absent: npm run executes a named script, not lifecycle hooks above the build_cmd[1:] assertion would make the asymmetry self-documenting and prevent a well-meaning contributor from "fixing" the apparent inconsistency.

[NB-2] scripts/hatch_build.py:148--ignore-scripts suppresses the root package's own prepare script if one is ever added

npm ci --ignore-scripts suppresses ALL lifecycle scripts, including the root package's prepare entry. Currently web/frontend/package.json has no prepare script so this is benign. However, if a future contributor adds "prepare": "some-setup-tool" to the root package.json (a common pattern for husky, lefthook, etc.), the flag will silently suppress it during wheel builds without any visible error. The code comment at line 141–147 explains the security rationale well, but does not call out this constraint. Adding "Note: if web/frontend/package.json ever gains a prepare script, verify it is not needed at wheel-build time" would surface the constraint before it bites.

Nits

  • [NIT-1] tests/test_build_hook.py:189 — the docstring of test_passes_resolved_npm_path_not_bare_name still describes only the "Bug 1" Windows .cmd fix. Now that this test also covers the GHSA-pfvh regression, expanding the docstring to mention both responsibilities (or splitting into two focused tests) would keep the test naming honest.

Verification log

  • gh pr view 461 --json title,body,files,additions,deletions,state → state=OPEN, 2 files (+12/-1), conventional fix(build): prefix ✓
  • git rev-parse --abbrev-ref HEADfix/build-ignore-scripts matches PR branch ✓
  • git show 7cec496 --stat → only scripts/hatch_build.py (+9/-1) and tests/test_build_hook.py (+4/0) touched; no src/ changes ✓ (Plugin synchronization map: no CLI command surface change, no OPERATION_REGISTRY / context.py / CLAUDE.md / keboola-expert.md updates required)
  • Layer violation checks (typer in services, httpx in commands, formatter in clients): grep returned empty ✓
  • Convention checks (magic numbers, raw error_code strings, bare except, print() in src/, token in logs): all empty ✓
  • python3 scan of web/frontend/package-lock.json (lockfileVersion=3, 393 packages): 0 packages with lifecycle scripts (postinstall/install/preinstall/prepare) ✓ — confirms --ignore-scripts has nothing to suppress in the current lock; the security value is defense-in-depth against a future compromised dependency
  • web/frontend/package.json root scripts: {dev, build, preview, test} — no prepare/postinstall entries ✓
  • No esbuild entries in lock file (vite 8 / rolldown does not depend on esbuild's postinstall binary fetcher) ✓ — the classic --ignore-scripts breakage vector is absent
  • No git-hook managers (husky, lefthook, simple-git-hooks) in dependencies ✓
  • uv run pytest tests/test_build_hook.py::TestBundleUiBug1NpmInvocation::test_passes_resolved_npm_path_not_bare_name -v → 1 passed ✓
  • make check4177 passed, 8 skipped, lint/format/typecheck/skill/version/error-codes clean ✓
  • Behavior verification: the PR author ran the real build with the flag (npm ci --ignore-scripts ... && npm run builddist/index.html present); the lock file scan above independently corroborates that no lifecycle script would have fired anyway; cannot re-run the full build in this reviewer context (no npm in CI sandbox), but the evidence is sufficient

Open questions for the author

(none)

…GHSA-pfvh)

The hatch build hook runs `npm ci` at wheel-build / `git+` install time. Without
--ignore-scripts, a compromised (transitive) npm dependency's postinstall would
execute arbitrary code on the user's machine during install -- a real
install-time RCE surface.

Add --ignore-scripts. Verified non-breaking: `tsc -b && vite build` needs no
dependency install-scripts (vite 8 / rolldown ships prebuilt platform binaries,
not postinstall downloads) -- ran the real build with the flag and it produced
dist/index.html cleanly. test_build_hook asserts the flag is present.

Code-only (no version bump / changelog) to stay conflict-free against the rapid
main release cadence; version + changelog added at next release.

Private advisory GHSA-pfvh-6q5w-9m52.
@padak padak force-pushed the fix/build-ignore-scripts branch from 7cec496 to 35cb8b5 Compare June 23, 2026 12:52
@padak

padak commented Jun 23, 2026

Copy link
Copy Markdown
Member Author

Author response — review findings addressed (commit 35cb8b5, still code-only)

Thanks — and for the empirical confirmation that package-lock.json (393 pkgs) carries zero lifecycle scripts today, so this is pure defense-in-depth. All three findings were "make the intentional choice discoverable so a future maintainer doesn't undo it", and I've added that discoverability:

  • NB-1 — undocumented ci vs build asymmetry in the test: Added an inline comment at the assertions explaining that --ignore-scripts intentionally gates only the dependency install (npm ci), and npm run build (our own build script) must NOT carry it. Also turned that into a tested invariant: assert "--ignore-scripts" not in build_cmd.

  • NB-2 — --ignore-scripts would also suppress a root prepare/postinstall: Added a NOTE in the hatch_build.py comment: there is no root build script today; if one is ever added AND required for the build, it must be invoked explicitly rather than dropping --ignore-scripts (which would re-open the dep-tree RCE surface).

  • NIT-1 — stale test docstring: Updated to state both regressions it now guards (the Windows npm.cmd path resolution AND the GHSA-pfvh --ignore-scripts flag).

No logic change. Still code-only (version + changelog deferred to the next release alongside #422 / #460). make check green; tests/test_build_hook.py 20 passed.

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