mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-01 14:05:43 +02:00
security: pass cwd to git via execFileSync, not interpolation through /bin/sh
`bin/gstack-memory-ingest.ts:632-643` ran `execSync(\`git -C ${JSON.stringify(cwd)}
remote get-url origin 2>/dev/null\`, ...)`. JSON.stringify escapes `"` and `\`
but not `$` or backticks, so a `cwd` of `"$(touch /tmp/marker)"` survived JSON
quoting and detonated under /bin/sh's command-substitution-inside-double-quotes.
`cwd` originates from transcript JSONL records under
`~/.claude/projects/<encoded-cwd>/<uuid>.jsonl` and
`~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`. The walker grabs the first
`.cwd` it sees per session. That's an untrusted surface in the gstack threat
model — the L1-L6 sidebar security stack exists exactly because agent
transcripts can carry attacker-influenced text. Two pivots above the local
same-uid bar: (a) prompt-injection appending `cwd="$(...)"` to the active
session log turns the next /sync-gbrain run into RCE under the user's uid;
(b) cross-machine transcript share (a colleague's `.claude/projects` snippet
untar'd into HOME, a documented gbrain dogfooding shape) → RCE on first sync.
Fix swaps the one execSync for `execFileSync("git", ["-C", cwd, "remote",
"get-url", "origin"], ...)`. No shell, argv passed directly to git. The same
module already uses execFileSync for `gbrainAvailable()` (line 762 pre-patch)
and `gbrainPutPage()` (line 816 pre-patch) — this single execSync was the
outlier.
Test: `gstack-memory-ingest security: untrusted cwd cannot trigger shell
substitution` plants a Claude-Code-shaped JSONL with cwd=`$(touch <marker>)`
and asserts the marker file is not created after `--incremental --quiet`.
Negative control: with the patch reverted, the test fails (marker created);
with the patch applied, it passes (18/18 in test/gstack-memory-ingest.test.ts).
This commit is contained in:
@@ -632,9 +632,16 @@ function extractContentText(rec: any): string {
|
||||
function resolveGitRemote(cwd: string): string {
|
||||
if (!cwd) return "";
|
||||
try {
|
||||
const out = execSync(`git -C ${JSON.stringify(cwd)} remote get-url origin 2>/dev/null`, {
|
||||
// execFileSync (no shell) so `cwd` cannot trigger command substitution.
|
||||
// Transcript JSONL records are an untrusted surface (a poisoned `.cwd`
|
||||
// value containing `"$(...)"` survived `JSON.stringify` interpolation
|
||||
// into a `/bin/sh -c` context, since JSON quoting does not escape `$`
|
||||
// or backticks). Mirrors the execFileSync pattern this module already
|
||||
// uses for `gbrainAvailable()` (line 762) and `gbrainPutPage()` (line 816).
|
||||
const out = execFileSync("git", ["-C", cwd, "remote", "get-url", "origin"], {
|
||||
encoding: "utf-8",
|
||||
timeout: 2000,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
return canonicalizeRemote(out.trim());
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user