fix(B19): allowlist binaries in verify-changes, add --allow-arbitrary-tool, npx --no-install (#22) - #69
Open
bigknoxy wants to merge 1 commit into
Open
Conversation
…-tool, npx --no-install (#22) Security hardening for verify-changes (issue #22, score 50, P1). verify-changes now blocks execution of arbitrary binaries: - Allowlist of ~25 known-safe verification tools; anything else is rejected with a clear error listing the allowed set and the override flag. - --allow-arbitrary-tool opt-in for non-standard tools (warns on stderr). - TEST_RUNNER_MAP uses npx --no-install so vitest/jest cannot trigger network installs of arbitrary packages when missing locally. - Spawn always uses argv array (never shell: true); shell metacharacters like $(), backticks, and pipes are inert literal arguments. - Resolved command line is logged to stderr before each execution. - Auto-detected tools from the target repo's config are announced on stderr so the resolved command is visible even without an explicit flag. Fixes #22
bigknoxy
commented
Aug 15, 2026
bigknoxy
left a comment
Owner
Author
There was a problem hiding this comment.
Hermes Agent Code Review — PR #69
Verdict: Comment (security-critical fix, well-tested; minor suggestions only)
OK Looks Good
- Binary allowlist (
ALLOWED_BINARIES) is the right fix for the B19 supply-chain vulnerability: rejects arbitrary binaries by default, with an explicit opt-in via--allow-arbitrary-tool. npx --no-installinTEST_RUNNER_MAPeliminates the network-fetch attack vector for vitest/jest.resolveCommandextracts the basename from path arguments (e.g../node_modules/.bin/xis resolved), preventing path-based bypasses.runToollogs the resolved command line to stderr before execution — excellent for auditability.- 9 new regression tests cover: allowlist rejection,
--allow-arbitrary-tool=trueoverride,npx --no-installenforcement, shell metacharacter safety (canary file test for$(touch ...)), and auto-detected tool logging. The test suite is comprehensive for a security boundary. - 35 tests pass (25 existing + 9 new).
Warnings / Suggestions
resolveCommandsplits on whitespace (cmd.split(/\s+/)) — this breaks if a binary path contains spaces. The allowlist check usesbinary.split(/[\\/]/).pop()to get the basename, but if the binary itself has a space in its path, the split will mangle it. This is an edge case but worth noting in a comment.- Allowlist is hard-coded in source — adding new tools (e.g.,
next,svelte-check,astro) requires a code change + release. Consider whether this should be configurable via env var for projects with unusual toolchains. The PR description says ~25 tools; this may grow over time. - The test for shell metacharacters (
echo $(touch ${canary})) is clever, but note thatechowith$(...)as a literal argument still prints the string — it just doesn't execute it. This is the correct behavior withshell: false, but verify Bun.spawn doesn't have any shell fallback. go vetis missing fromEXT_TOOLS—.gofiles getgo vettypecheck from the extension fallback, butgois in the allowlist. However,go vetpasses throughresolveCommandandgois allowlisted, so this is fine — just confirming the chain works.
Suggestions
- Consider a test asserting that a path-traversal attempt (e.g.,
--formatter "../../bin/evil") is rejected. The basename extraction would resolve toevilwhich is not on the allowlist, so it should fail — worth an explicit test. - The
--allow-arbitrary-toolflag warns on each use — consider whether the CLI help text makes the security tradeoff clear enough.
Reviewed by Hermes Agent
bigknoxy
commented
Aug 16, 2026
bigknoxy
left a comment
Owner
Author
There was a problem hiding this comment.
Code Review Summary
Verdict: Comment — strong P1 security fix. I verified all 35 tests pass locally (including 9 new B19 regression tests). One minor hardening nudge.
Critical
(None — the security holes are fixed, not introduced.)
Warnings
- src/core/verify.ts — allowlist approach is sound, but consider documenting the threat model. The PR correctly fixes three vectors: binary allowlist (default deny),
npx --no-installto kill supply-chain fetches, and argv-array spawning (already inert to shell metacharacters). The regression test for$()/ canary confirms metacharacters stay literal (no canary file created on my run). One follow-up worth filing: the allowlist checks the basename of the binary. A symlink with a benign name pointing to a malicious binary would pass. This is low-likelihood (requires write access to a path on PATH or cwd), but worth a short note in the allowlist comment for future audit readers.
Suggestions
- resolveCommand: consider logging the rejected binary name too. Currently
runToollogs the resolved command line on success but the allowlist rejection returns{ passed: false, output: "security: ..." }without the canonical[verify-changes] running:line. For observability parity, you could emit[verify-changes] blocked: <binaryName> (allowlist)to stderr. Minor. - The
echomock binary now requiresallowArbitraryTool: truein several existing tests (echo is not on the allowlist). This is correct and the PR updates those call sites. Good catch keeping them passing. - Consider extending the shell-metacharacter regression to backtick variants (``) and
|pipes explicitly, though the argv-spawn fix already covers them structurally.
Looks Good
- Default-deny allowlist design with an explicit
--allow-arbitrary-toolescape hatch is the correct security posture. npx --no-installon both vitest and jest entries closes the supply-chain vector cleanly.- Clear
[verify-changes] running: <cmd>logging on stderr before execution — great for auditability. - Auto-detected tool announcement on stderr gives the caller visibility into target-repo-chosen tools.
- Test naming ("B19 — verify-changes security hardening") makes the security intent explicit.
Reviewed by Hermes Agent.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
Issue #22 (B19) —
verify-changesexecutes arbitrary target-repo-chosen binaries. Score 50, P1, area:security.The problem
Bun.spawnreceived untrusted string arguments from three sources:--formatter,--linter,--test-runnervalues handed directly to spawnnpxinstalls from the network: A typo or attacker-chosen runner name was a supply-chain execution since npm registry downloads + execute packages on first usepackage.json: Repo being edited chose what code executesWhat changed
1. Binary allowlist (default: reject)
allowArbitraryTool: truefor mock tools (echo,sleep) since they aren't testing security2.
npx --no-installTEST_RUNNER_MAPnow usesnpx --no-install vitest runandnpx --no-install jest3. Shell injection is inert
shell: true)$(), backticks, and pipes are treated as literal arguments4. Resolved command line logged to stderr before execution
Every spawned command prints
[verify-changes] running: <binary> <full args>so the caller can see exactly what about to execute.5. Auto-detected tool flagged on stderr
When auto-detect fills in a tool from project config (not explicit flag), it announces this on stderr.
Testing
--linter "curl http://evil.example.com"rejected without--allow-arbitrary-toolFiles changed
src/core/verify.tssrc/cli.ts--allow-arbitrary-toolflag addedtests/verify.test.tsdocs/CLI-QUICKREF.mdFixes #22