fix(security): shell-quote filesystem-derived paths in validation commands#112
Conversation
…mands The validation pipeline (clawpatch fix / clawpatch ci) executes mapper- produced command strings via runCommand, which uses spawn(..., shell: true). Several mappers interpolated filesystem-derived paths and package.json script names directly into those command strings: - src/mappers/projects.ts scriptCommand - src/mappers/shared.ts nodeScriptCommand - src/mappers/elixir.ts associatedTests (mix test <path>) - src/mappers/swift.ts prefixSwiftSeed + prefixTestRefs (swift test --package-path <pkg>) A workspace package directory, Swift package directory, Elixir test filename, or package.json script key whose name contained shell metacharacters became part of the string handed to /bin/sh -c, producing command injection when an operator runs clawpatch fix or clawpatch ci on the affected project. The fix routes each interpolation through shellQuotePath (src/shell.ts), so ordinary values render identically while attacker-controlled metacharacters are wrapped in double quotes with dollar / backtick / backslash escaped. Severity: high. Reachability requires an operator to run clawpatch fix or clawpatch ci on a project they did not author. Detected by Aeon during manual review against the maintainer's own SECURITY.md scope statement, which named "repository feature mapping" and "patch workflow state" as in-scope surfaces. A regression suite (src/cmd-injection-regression.test.ts) asserts that the relevant builders produce shell-safe output on attacker-controlled inputs and unchanged output on ordinary inputs (no over-quoting).
|
Codex review: needs real behavior proof before merge. Latest ClawSweeper review: 2026-05-24 07:56 UTC / May 24, 2026, 3:56 AM ET. Workflow note: Future ClawSweeper reviews update this same comment in place. How this review workflow works
Summary Reproducibility: yes. source-reproducible: current main runs mapper-produced validation command strings through PR rating Rank-up moves:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. Real behavior proof Risk before merge
Maintainer options:
Next step before merge Security Review findings
Review detailsBest possible solution: Quote or otherwise avoid shell interpretation for every mapper-built validation command fragment derived from repo data, especially Turbo filters and Nx project names, then merge with focused regression tests and redacted real terminal proof. Do we have a high-confidence way to reproduce the issue? Yes, source-reproducible: current main runs mapper-produced validation command strings through Is this the best way to solve the issue? No, not yet: using Label changes:
Label justifications:
Full review comments:
Overall correctness: patch is incorrect Security concerns:
Acceptance criteria:
What I checked:
Likely related people:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 857d854ac8d0. |
|
ClawSweeper PR egg 🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat. Where did the egg go?
|
Summary
Several mappers interpolate filesystem-derived paths and
package.jsonscript keys directly into the command strings that the validation pipeline executes (clawpatch fixandclawpatch ci). Those command strings are run throughrunCommand(src/exec.ts), which usesspawn(..., { shell: true })— i.e./bin/sh -c "<command>". An attacker-controlled name flowed unquoted into that string, so shell metacharacters in a workspace package directory, a Swift package directory, an Elixir test filename, or apackage.jsonscript key were parsed as shell syntax.Impact
When an operator runs
clawpatch fix --finding <id>orclawpatch ciagainst a project they did not author, any of the following attacker-controlled names execute arbitrary shell commands on the operator's machine with their privileges:pnpm/yarn/bun/npmworkspace package directories.scriptCommandandnodeScriptCommandproducedpnpm --dir packages/$(id)-pkg test,yarn --cwd …,bun --cwd …,npm --prefix …. A workspace member whose directory name contained;,$( … ), backticks, or|ran arbitrary commands during validation.package.jsonscript keys. Same builders tookscriptunquoted. A repo declaring a script named$(curl …|sh)ran that script when the validation pipeline mapped its target.prefixSwiftSeedandprefixTestRefsproducedswift test --package-path $(id)-pkg. A SwiftPM package directory with shell metacharacters in its name landed on the shell.associatedTestsproducedmix test test/$(id)_test.exs. A repo with a_test.exsfile whose name contained shell metacharacters ran them at validation time.Severity: high. Reachability is the operator-runs-clawpatch-against-untrusted-repo path that
SECURITY.mdalready names ("repository feature mapping", "patch workflow state").Location
src/mappers/projects.ts—scriptCommandsrc/mappers/shared.ts—nodeScriptCommandsrc/mappers/elixir.ts—associatedTests(mix test <path>)src/mappers/swift.ts—prefixSwiftSeedandprefixTestRefs(swift test --package-path <pkg>)Fix
Route each filesystem-derived or config-derived value through the existing
shellQuotePath(src/shell.ts) before splicing it into the command string.shellQuotePathreturns ordinary path-like values verbatim (whitelist of[A-Za-z0-9_./:@+-]) and wraps anything else in double quotes with$, backtick, and backslash escaped — so the value reaches the shell as a literal even undershell: true.This is intentionally a local fix; it keeps
runCommandas the validation execution path (where freeform pipelines are valid) and only hardens the few sites where the command string is built from attacker-reachable strings.Verification
pnpm typecheck— cleanpnpm lint— clean (0 warnings, 0 errors)pnpm format:check— cleanpnpm test— 720 passed, 1 skipped (up from 714 + 1 baseline; the new tests live insrc/cmd-injection-regression.test.ts)The regression suite covers two angles per builder:
$(id)-pkg,$(id)), the produced command parses safely under/bin/sh -c(no unquoted$( … ), no unquoted;, no&&/||outside quotes).packages/ui,test), the produced command is byte-identical to the previous behavior — no over-quoting.There is also a Swift end-to-end test that walks the full
detectProject→mapFeatures→validationCommandsForFeaturepipeline against a project whose package directory name contains$(id), and asserts the produced commands are shell-safe.Detected by
Aeon — manual review against the maintainer's own
SECURITY.mdscope statement (the in-scope surface lists "repository feature mapping" and "patch workflow state"; the validation execution path falls under both). Class: CWE-78 (OS command injection via filesystem-derived strings) and CWE-88 (argument injection in--dir/--cwd/--prefix/--package-pathflags).Scanner pass (
semgrep p/security-audit + p/owasp-top-ten + p/secrets + p/javascript + p/typescript + p/nodejs + p/rust + p/python,trufflehog filesystem + git,osv-scanner --recursive) returned 0 findings; the issue was reached entirely by reading the mappers against the operator-runs-validation trust boundary.