diff --git a/.abcd/work/issues/resolved/iss-2610090821313095-guard-function-keyword-body-only-warns.md b/.abcd/work/issues/resolved/iss-2610090821313095-guard-function-keyword-body-only-warns.md index cf4dcb2ac..89fe95e91 100644 --- a/.abcd/work/issues/resolved/iss-2610090821313095-guard-function-keyword-body-only-warns.md +++ b/.abcd/work/issues/resolved/iss-2610090821313095-guard-function-keyword-body-only-warns.md @@ -5,7 +5,7 @@ slug: "guard-function-keyword-body-only-warns" severity: "minor" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-6h89-gjcg-3h3h, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/core/guard/match.go" @@ -16,6 +16,8 @@ impact: fix `abcd guard` only warns on a bash `function f { ...; }; f` whose body is a blocker, and the PreToolUse hook lets a warning run, so the function executes. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-6h89-gjcg-3h3h (draft, severity medium). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `function` is not in the reserved-word set the walk steps over before command position (internal/core/guard/match.go:158, `reserved`), so the keyword form falls to unrecognised-launcher while the POSIX form `f() { ...; }` has its body judged. The hook maps a warn to exit 1, which surfaces the message and lets the tool run; only a block (exit 2) stops it (internal/surface/cli/guard.go:455-466). \ No newline at end of file +Evidence (lines at main 7549ca2d5): `function` is not in the reserved-word set the walk steps over before command position (internal/core/guard/match.go:158, `reserved`), so the keyword form falls to unrecognised-launcher while the POSIX form `f() { ...; }` has its body judged. The hook maps a warn to exit 1, which surfaces the message and lets the tool run; only a block (exit 2) stops it (internal/surface/cli/guard.go:455-466). + +Reproduction: `Defaults().Check("function f { git push --force origin main; }; f")` is warn / unrecognised-launcher; `Defaults().Check("f() { git push --force origin main; }; f")` is block / git-push-force. On /bin/bash 3.2.57, `/bin/bash -c "function f { touch $MARK; }; f"` creates the mark. diff --git a/.abcd/work/issues/resolved/iss-2610090821476887-guard-allows-trap-command-string.md b/.abcd/work/issues/resolved/iss-2610090821476887-guard-allows-trap-command-string.md index 2a0a97a92..a37bc4d7b 100644 --- a/.abcd/work/issues/resolved/iss-2610090821476887-guard-allows-trap-command-string.md +++ b/.abcd/work/issues/resolved/iss-2610090821476887-guard-allows-trap-command-string.md @@ -5,7 +5,7 @@ slug: "guard-allows-trap-command-string" severity: "major" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-f789-v342-57jr, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/core/guard/payload.go" @@ -16,6 +16,8 @@ impact: fix `abcd guard` allows a blocker written as the command string of `trap`, and bash runs it, an EXIT trap needing no further command. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-f789-v342-57jr (draft, severity high). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `evalPayload` joins `eval`'s operands and the guard re-reads that string, which is why `eval -- 'git push --force origin main'` is a block (internal/core/guard/payload.go:1578). Nothing walks the command operand of `trap`. The hook maps an allow to exit 0 and only a block to exit 2 (internal/surface/cli/guard.go:455-470). \ No newline at end of file +Evidence (lines at main 7549ca2d5): `evalPayload` joins `eval`'s operands and the guard re-reads that string, which is why `eval -- 'git push --force origin main'` is a block (internal/core/guard/payload.go:1578). Nothing walks the command operand of `trap`. The hook maps an allow to exit 0 and only a block to exit 2 (internal/surface/cli/guard.go:455-470). + +Reproduction: `Defaults().Check` returns allow for `trap 'git push --force origin main' EXIT`, the same with `; true` appended, `trap -- '...' EXIT; true`, `trap '...' 0; true` and `trap '...' DEBUG; true`. On /bin/bash 3.2.57, `/bin/bash -c "trap 'touch $MARK' EXIT"` creates the mark; the DEBUG form runs when a later command is present. The same text under `eval` is block / git-push-force. diff --git a/.abcd/work/issues/resolved/iss-2610090821484829-guard-allows-bash-env-sourced-file.md b/.abcd/work/issues/resolved/iss-2610090821484829-guard-allows-bash-env-sourced-file.md index 15c4795b0..ced37252f 100644 --- a/.abcd/work/issues/resolved/iss-2610090821484829-guard-allows-bash-env-sourced-file.md +++ b/.abcd/work/issues/resolved/iss-2610090821484829-guard-allows-bash-env-sourced-file.md @@ -5,7 +5,7 @@ slug: "guard-allows-bash-env-sourced-file" severity: "major" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-r2w5-wf2r-jmf8, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/core/guard/payload.go" @@ -16,6 +16,8 @@ impact: fix `abcd guard` allows `BASH_ENV= bash -c true`, and non-interactive bash sources that file before `-c`, so a blocker written to the file in the same command runs. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-r2w5-wf2r-jmf8 (draft, severity high). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `shellReadsStream` treats a `-c` string as the payload and does not look at an assignment prefix for `BASH_ENV` (internal/core/guard/payload.go:1860). The stream block's own successor tells the caller "to run a script, save it and run it as a file after reading it" (internal/core/guard/payload.go:1971), and the file form it recommends is also an allow that bash runs, so closing `BASH_ENV` alone leaves the file channel open. \ No newline at end of file +Evidence (lines at main 7549ca2d5): `shellReadsStream` treats a `-c` string as the payload and does not look at an assignment prefix for `BASH_ENV` (internal/core/guard/payload.go:1860). The stream block's own successor tells the caller "to run a script, save it and run it as a file after reading it" (internal/core/guard/payload.go:1971), and the file form it recommends is also an allow that bash runs, so closing `BASH_ENV` alone leaves the file channel open. + +Reproduction: `printf '%s\n' 'git push --force origin main' > /tmp/e; BASH_ENV=/tmp/e bash -c true` is allow, and /bin/bash 3.2.57 runs the file (a `touch` stand-in creates the mark). The same allow and execution for `bash /tmp/s.sh` and `source /tmp/s.sh` after the same printf. diff --git a/.abcd/work/issues/resolved/iss-2610090821489740-guard-steps-over-bash-init-file.md b/.abcd/work/issues/resolved/iss-2610090821489740-guard-steps-over-bash-init-file.md index 2d5145849..a17f9fc0f 100644 --- a/.abcd/work/issues/resolved/iss-2610090821489740-guard-steps-over-bash-init-file.md +++ b/.abcd/work/issues/resolved/iss-2610090821489740-guard-steps-over-bash-init-file.md @@ -5,7 +5,7 @@ slug: "guard-steps-over-bash-init-file" severity: "major" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-xr66-hjpp-xwgc, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/core/guard/payload.go" @@ -17,6 +17,8 @@ impact: fix `abcd guard` steps over the value of `bash --init-file` and `--rcfile`, and interactive bash runs that file before `-c`, so a blocker in it runs while the checked command reads `bash -i -c true`. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-xr66-hjpp-xwgc (draft, severity high). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `shellReadsStream` knows the two options only to skip their value (internal/core/guard/payload.go:1914, the list `shellStreamValueOptions` at :1943). The same process substitution in script position is a block through `readsScriptStream` (internal/core/guard/payload.go:1816). \ No newline at end of file +Evidence (lines at main 7549ca2d5): `shellReadsStream` knows the two options only to skip their value (internal/core/guard/payload.go:1914, the list `shellStreamValueOptions` at :1943). The same process substitution in script position is a block through `readsScriptStream` (internal/core/guard/payload.go:1816). + +Reproduction: `Defaults().Check` returns allow for `bash --init-file <(printf '%s\n' 'git push --force origin main') -i -c true`, the same with `--rcfile`, and `bash --init-file /tmp/init.sh -i -c true` after printf writes the file. /bin/bash 3.2.57 runs each (a `touch` stand-in creates the mark). `bash <(printf '%s\n' 'git push --force origin main')` is block / interpreter-reads-stream. Related: the file-form decision in the BASH_ENV advisory GHSA-r2w5-wf2r-jmf8. diff --git a/.abcd/work/issues/resolved/iss-2610090821491948-scanner-misses-stacked-json-percent-encoding.md b/.abcd/work/issues/resolved/iss-2610090821491948-scanner-misses-stacked-json-percent-encoding.md index 3b5698866..a5624bc63 100644 --- a/.abcd/work/issues/resolved/iss-2610090821491948-scanner-misses-stacked-json-percent-encoding.md +++ b/.abcd/work/issues/resolved/iss-2610090821491948-scanner-misses-stacked-json-percent-encoding.md @@ -5,7 +5,7 @@ slug: "scanner-misses-stacked-json-percent-encoding" severity: "major" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-74v2-f6pg-p4fq, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/adapter/scanner/percent.go" @@ -16,6 +16,8 @@ impact: fix The secret scanner misses a token or home path written as a JSON escape stacked on a percent encoding (or the reverse), because it never runs one decoder on the other's output, so `ScanText` reports nothing, `Redact` leaves it, and the launch gate ships it. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-74v2-f6pg-p4fq (draft, severity high). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `lineViews` builds one percent view of the raw line and then JSON layers of that same raw line (internal/adapter/scanner/percent.go:61). The comment at internal/adapter/scanner/jsonescape.go:45 claims the two do not compose, yet `jsonUnescapeOnce` emits `%` as `%` (internal/adapter/scanner/jsonescape.go:100). `ScanBundle` counts the text file as fully scanned (internal/adapter/scanner/scanner.go:1225), and `scanRefusals` (internal/core/launch/dryrun.go:269) refuses only an unavailable scanner, a kept hard-fail or an Unscanned path. History and memory call the same `ScanText`. \ No newline at end of file +Evidence (lines at main 7549ca2d5): `lineViews` builds one percent view of the raw line and then JSON layers of that same raw line (internal/adapter/scanner/percent.go:61). The comment at internal/adapter/scanner/jsonescape.go:45 claims the two do not compose, yet `jsonUnescapeOnce` emits `%` as `%` (internal/adapter/scanner/jsonescape.go:100). `ScanBundle` counts the text file as fully scanned (internal/adapter/scanner/scanner.go:1225), and `scanRefusals` (internal/core/launch/dryrun.go:269) refuses only an unavailable scanner, a kept hard-fail or an Unscanned path. History and memory call the same `ScanText`. + +Reproduction: with T a `ghp_` prefix followed by 40 `q` characters, none of these hard-fails: `token=%67` followed by T without its first character (JSON-unescape then percent-decode yields T); `token=%5Cu0067` with the same tail (the reverse order); `see %2F in the log` for the probed home path (no home_path_self). The plaintext token, a single `%67` and a single `g` of the first byte still match. diff --git a/.abcd/work/issues/resolved/iss-2610090821499579-scanner-png-chunk-tail-after-zlib-unscanned.md b/.abcd/work/issues/resolved/iss-2610090821499579-scanner-png-chunk-tail-after-zlib-unscanned.md index 9e28e15bf..7eec43777 100644 --- a/.abcd/work/issues/resolved/iss-2610090821499579-scanner-png-chunk-tail-after-zlib-unscanned.md +++ b/.abcd/work/issues/resolved/iss-2610090821499579-scanner-png-chunk-tail-after-zlib-unscanned.md @@ -5,7 +5,7 @@ slug: "scanner-png-chunk-tail-after-zlib-unscanned" severity: "major" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-83x2-mf5v-j796, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/adapter/scanner/container.go" @@ -16,6 +16,8 @@ impact: fix The secret scanner reports a PNG as content-decoded while bytes inside a compressed chunk after the zlib checksum are never inflated or scanned, so a gzip member there ships through the launch gate. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-83x2-mf5v-j796 (draft, severity high). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `decodeBudget.inflate` uses `zlib.NewReader`, which stops at the Adler-32, and returns only the inflated bytes (internal/adapter/scanner/container.go:236). The `zTXt` arm covers only that output (internal/adapter/scanner/container.go:970) and `decodePNG` returns decoded after IEND (internal/adapter/scanner/container.go:951). `decodeStream`, the top-level zlib path, does cover the unread tail (internal/adapter/scanner/container.go:329). The same unread tail exists for compressed `iTXt`, `iCCP` and `IDAT`. Distinct from a private security report (the skip that never opened the PNG). A PNG-only bundle is refused by the zero-coverage sentinel; the bypass needs one other full-text file, which the include list already has. \ No newline at end of file +Evidence (lines at main 7549ca2d5): `decodeBudget.inflate` uses `zlib.NewReader`, which stops at the Adler-32, and returns only the inflated bytes (internal/adapter/scanner/container.go:236). The `zTXt` arm covers only that output (internal/adapter/scanner/container.go:970) and `decodePNG` returns decoded after IEND (internal/adapter/scanner/container.go:951). `decodeStream`, the top-level zlib path, does cover the unread tail (internal/adapter/scanner/container.go:329). The same unread tail exists for compressed `iTXt`, `iCCP` and `IDAT`. Distinct from GHSA-9wv7-88w3-f77m (the skip that never opened the PNG). A PNG-only bundle is refused by the zero-coverage sentinel; the bypass needs one other full-text file, which the include list already has. + +Reproduction: build a small valid PNG and insert a CRC'd `zTXt` chunk before IEND: keyword `Comment`, NUL, compression method 0, a zlib member of `harmless` plus a newline, then a Huffman-coded gzip member of a `ghp_` token. Scan it beside README.md: `ContentDecoded` lists the PNG and `HardFails` is 0; gunzip of the tail returns the token. diff --git a/.abcd/work/issues/resolved/iss-2610090821502084-scanner-text-sniff-8192-bytes-counts-as-scanned.md b/.abcd/work/issues/resolved/iss-2610090821502084-scanner-text-sniff-8192-bytes-counts-as-scanned.md index def7a0e3e..0ea673b60 100644 --- a/.abcd/work/issues/resolved/iss-2610090821502084-scanner-text-sniff-8192-bytes-counts-as-scanned.md +++ b/.abcd/work/issues/resolved/iss-2610090821502084-scanner-text-sniff-8192-bytes-counts-as-scanned.md @@ -5,7 +5,7 @@ slug: "scanner-text-sniff-8192-bytes-counts-as-scanned" severity: "major" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-jwgh-838h-jrw9, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/adapter/scanner/scanner.go" @@ -16,6 +16,8 @@ impact: fix The secret scanner counts a file as fully scanned when only its first 8192 bytes are valid UTF-8 text, so a gzip member after a page of prose in an included markdown file ships with no finding. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-jwgh-838h-jrw9 (draft, severity high). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `isText` sniffs 8192 bytes (internal/adapter/scanner/scanner.go:1519). The text branch then runs `ScanText` on the raw bytes and increments FilesScanned without calling `decodeContent` (internal/adapter/scanner/scanner.go:1221-1225). `cover` already refuses an 8192-byte sniff as coverage of a decoded region. `docs/` is an include, and the bundler does not inspect content before inclusion. \ No newline at end of file +Evidence (lines at main 7549ca2d5): `isText` sniffs 8192 bytes (internal/adapter/scanner/scanner.go:1519). The text branch then runs `ScanText` on the raw bytes and increments FilesScanned without calling `decodeContent` (internal/adapter/scanner/scanner.go:1221-1225). `cover` already refuses an 8192-byte sniff as coverage of a decoded region. `docs/` is an include, and the bundler does not inspect content before inclusion. + +Reproduction: write about 10500 bytes of ASCII prose, then append a Huffman-coded gzip of a `ghp_` token (CPython `gzip.compress` or `gzip -nc`; Go's flate writer often leaves a short token as literals, which is caught). `ScanBundle` reports FilesScanned=1, HardFails=0, empty Unscanned and empty ContentUnverified; `gzip -dc` of the tail returns the token. The same bytes named `.gz` hard-fail. diff --git a/.abcd/work/issues/resolved/iss-2610090821506490-scanner-dot-skip-fragment-passes-addresses.md b/.abcd/work/issues/resolved/iss-2610090821506490-scanner-dot-skip-fragment-passes-addresses.md index 4e1be21ba..bb83b7406 100644 --- a/.abcd/work/issues/resolved/iss-2610090821506490-scanner-dot-skip-fragment-passes-addresses.md +++ b/.abcd/work/issues/resolved/iss-2610090821506490-scanner-dot-skip-fragment-passes-addresses.md @@ -5,7 +5,7 @@ slug: "scanner-dot-skip-fragment-passes-addresses" severity: "minor" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-8fqr-pv94-5jwp, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/adapter/scanner/scanner.go" @@ -16,6 +16,8 @@ impact: fix A committed `.abcd/config/pii.json` with `skip_path_fragments: ["."]` sends every dotted path to the byte-only branch, so a non-reserved IPv4, IPv6 or MAC address in an included file ships through the launch scan. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-8fqr-pv94-5jwp (draft, severity medium). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `mergeConfig` drops a fragment only when trimming slashes and whitespace leaves it empty, so `.` is stored (internal/adapter/scanner/scanner.go:338). `skipByFragment` is `strings.Contains` (internal/adapter/scanner/scanner.go:1495). `secretPatterns` drops the identity kinds, which include the network kinds (internal/adapter/scanner/scanner.go:1474). The zero-coverage sentinel trips only when FilesScanned is zero, and an undotted `LICENSE` on the include list keeps it above zero. `scanRefusals` does not refuse ContentUnverified files (internal/core/launch/dryrun.go:269). A token, the caller's home path, a real email and a long real name on that path still block. \ No newline at end of file +Evidence (lines at main 7549ca2d5): `mergeConfig` drops a fragment only when trimming slashes and whitespace leaves it empty, so `.` is stored (internal/adapter/scanner/scanner.go:338). `skipByFragment` is `strings.Contains` (internal/adapter/scanner/scanner.go:1495). `secretPatterns` drops the identity kinds, which include the network kinds (internal/adapter/scanner/scanner.go:1474). The zero-coverage sentinel trips only when FilesScanned is zero, and an undotted `LICENSE` on the include list keeps it above zero. `scanRefusals` does not refuse ContentUnverified files (internal/core/launch/dryrun.go:269). A token, the caller's home path, a real email and a long real name on that path still block. + +Reproduction: commit `{"skip_path_fragments":["."]}` as `.abcd/config/pii.json`, put a line of `dns ` followed by a public, non-reserved IPv4 address (the well-known public DNS resolver of four eights; capture redacted the literal) in `commands/a.md`, leave LICENSE clean. `ScanBundle` is available, FilesScanned is at least 1, findings are empty and the markdown file is ContentUnverified. `ScanText` of the same line is net:ipv4 at hard_fail. diff --git a/.abcd/work/issues/resolved/iss-2610090821510097-implement-sync-merge-runs-repo-merge-driver.md b/.abcd/work/issues/resolved/iss-2610090821510097-implement-sync-merge-runs-repo-merge-driver.md index 6fe652fbc..a00053770 100644 --- a/.abcd/work/issues/resolved/iss-2610090821510097-implement-sync-merge-runs-repo-merge-driver.md +++ b/.abcd/work/issues/resolved/iss-2610090821510097-implement-sync-merge-runs-repo-merge-driver.md @@ -5,7 +5,7 @@ slug: "implement-sync-merge-runs-repo-merge-driver" severity: "minor" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-486x-8h9p-pq5x, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/core/implement/loop/sync.go" @@ -16,6 +16,8 @@ impact: fix The implement loop's lane sync merge (`git merge --no-ff` through `pickGit`) runs a repository-configured merge driver, which executes as the operator and writes the merge result git keeps, although the loop presents this merge as running no hook code. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-486x-8h9p-pq5x (draft, severity medium). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `syncLane` (internal/core/implement/loop/sync.go:66) merges with `pickGit(lane.Worktree, "merge", "--no-ff", "--no-edit", "-m", msg, merged)` (internal/core/implement/loop/sync.go:95). `pickGit` pins hooks, fsmonitor and quotePath and runs under ScrubbedEnv, so repo and global config stay (internal/core/implement/loop/pickcommit.go:71). `landStage` calls `syncLane` only while `lane.Landing` is nil (internal/core/implement/loop/land.go:131, :139). The other side can come from a planted `refs/remotes/origin/`; no network remote is needed. \ No newline at end of file +Evidence (lines at main 7549ca2d5): `syncLane` (internal/core/implement/loop/sync.go:66) merges with `pickGit(lane.Worktree, "merge", "--no-ff", "--no-edit", "-m", msg, merged)` (internal/core/implement/loop/sync.go:95). `pickGit` pins hooks, fsmonitor and quotePath and runs under ScrubbedEnv, so repo and global config stay (internal/core/implement/loop/pickcommit.go:71). `landStage` calls `syncLane` only while `lane.Landing` is nil (internal/core/implement/loop/land.go:131, :139). The other side can come from a planted `refs/remotes/origin/`; no network remote is needed. + +Reproduction: on git 2.39.5, set `.git/info/attributes` to `* merge=evil` (or a committed `.gitattributes`) and `merge.evil.driver` to a script; point `refs/remotes/origin/` at a commit editing a file the lane also edits; run `abcd implement step` until the lane reaches `syncLane` with no landing. The script runs, git reports success and the worktree file holds the script's output; the post-merge hook does not run. With no overlapping change the driver does not start. diff --git a/.abcd/work/issues/resolved/iss-2610090821512707-scanner-tar-long-name-body-unscanned.md b/.abcd/work/issues/resolved/iss-2610090821512707-scanner-tar-long-name-body-unscanned.md index a9e397778..32d638492 100644 --- a/.abcd/work/issues/resolved/iss-2610090821512707-scanner-tar-long-name-body-unscanned.md +++ b/.abcd/work/issues/resolved/iss-2610090821512707-scanner-tar-long-name-body-unscanned.md @@ -5,7 +5,7 @@ slug: "scanner-tar-long-name-body-unscanned" severity: "major" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-jx54-228j-mf98, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/adapter/scanner/container.go" @@ -16,6 +16,8 @@ impact: fix The secret scanner reports a tar archive as content-decoded while the bytes after the first NUL of a GNU long-name (`././@LongLink`, type L) body are consumed by the tar reader and never scanned, so a gzip member there ships. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-jx54-228j-mf98 (draft, severity high). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `decodeTar` scans only what `archive/tar.Reader.Next` returns (internal/adapter/scanner/container.go:787); `Next` consumes the type-L body, keeps the C string before the first NUL as the next name and discards the rest. `coverTarHeader` scans `h.Name` only (internal/adapter/scanner/container.go:848), and the counting reader has already consumed the hidden bytes, so the trailer check misses them. `scanRefusals` treats ContentDecoded as covered and does not refuse ContentUnverified (internal/core/launch/dryrun.go:269), so returning not-decoded is not a fix. Distinct from a private security report. \ No newline at end of file +Evidence (lines at main 7549ca2d5): `decodeTar` scans only what `archive/tar.Reader.Next` returns (internal/adapter/scanner/container.go:787); `Next` consumes the type-L body, keeps the C string before the first NUL as the next name and discards the rest. `coverTarHeader` scans `h.Name` only (internal/adapter/scanner/container.go:848), and the counting reader has already consumed the hidden bytes, so the trailer check misses them. `scanRefusals` treats ContentDecoded as covered and does not refuse ContentUnverified (internal/core/launch/dryrun.go:269), so returning not-decoded is not a fix. Distinct from GHSA-9wv7-88w3-f77m. + +Reproduction: hand-build a ustar archive (Go's tar.Writer refuses a manual type-L header). First header `././@LongLink`, typeflag L, body `readme.txt`, NUL, then a gzip of long filler plus a `ghp_` token (not a literal in the raw archive). Second header a regular `readme.txt` of `harmless`. Two zero blocks. Scan beside README.md: ContentDecoded lists the tar and HardFails is 0; gunzip of the bytes after the NUL returns the token. diff --git a/.abcd/work/issues/resolved/iss-2610090821520843-pick-record-commit-runs-repo-signing-program.md b/.abcd/work/issues/resolved/iss-2610090821520843-pick-record-commit-runs-repo-signing-program.md index c07f56e67..897648bf7 100644 --- a/.abcd/work/issues/resolved/iss-2610090821520843-pick-record-commit-runs-repo-signing-program.md +++ b/.abcd/work/issues/resolved/iss-2610090821520843-pick-record-commit-runs-repo-signing-program.md @@ -5,17 +5,19 @@ slug: "pick-record-commit-runs-repo-signing-program" severity: "minor" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-4j38-5p8w-gf5r, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/core/implement/loop/pickcommit.go" -remedy: "Pass `-c commit.gpgsign=false` as a `pickGit` argument before the `commit` subcommand; prove it with a loop test (watched fail first) that the picked record commit leaves the gpg.program, gpg.ssh.program and defaultKeyCommand marks empty and still succeeds, the pre-commit hook still does not run, and a clean filter for a real LFS path still runs; sweep siblings (every commit, merge commit or tag abcd composes, where tag.gpgSign and the merge in a private security report can sign too)." +remedy: "Pass `-c commit.gpgsign=false` as a `pickGit` argument before the `commit` subcommand; prove it with a loop test (watched fail first) that the picked record commit leaves the gpg.program, gpg.ssh.program and defaultKeyCommand marks empty and still succeeds, the pre-commit hook still does not run, and a clean filter for a real LFS path still runs; sweep siblings (every commit, merge commit or tag abcd composes, where tag.gpgSign and the merge in GHSA-486x-8h9p-pq5x can sign too)." resolution: "The shared pin list gitutil.ExecPins now carries commit.gpgsign=false, and pickGit prepends that list instead of its own copy, so the pick's record commit and a sync's merge commit no longer start gpg.program, gpg.ssh.program or gpg.ssh.defaultKeyCommand; the commit is still made and content filters still run. TestPickGitStartsNoRepoSigningProgram proves it." impact: fix --- The picked implement run's hooks-off record commit runs the repository's signing program (`gpg.program`, `gpg.ssh.program` or `gpg.ssh.defaultKeyCommand`) as the operator when the checkout sets `commit.gpgsign=true`. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-4j38-5p8w-gf5r (draft, severity medium). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `pickCommit` (internal/core/implement/loop/pickcommit.go:92) calls `pickGit(wt, "commit", "-q", "-m", ..., "--", rel)` (internal/core/implement/loop/pickcommit.go:149). `pickGit` prepends only the hooksPath, fsmonitor and quotePath pins and runs under ScrubbedEnv (internal/core/implement/loop/pickcommit.go:71), while the comment at internal/core/implement/loop/pickcommit.go:20 says a hook dispatcher is code the loop does not run. Repo-local `commit.gpgsign=true` overrides a global false. The commit runs only for a picked run whose `Pick.Lane` is this lane. \ No newline at end of file +Evidence (lines at main 7549ca2d5): `pickCommit` (internal/core/implement/loop/pickcommit.go:92) calls `pickGit(wt, "commit", "-q", "-m", ..., "--", rel)` (internal/core/implement/loop/pickcommit.go:149). `pickGit` prepends only the hooksPath, fsmonitor and quotePath pins and runs under ScrubbedEnv (internal/core/implement/loop/pickcommit.go:71), while the comment at internal/core/implement/loop/pickcommit.go:20 says a hook dispatcher is code the loop does not run. Repo-local `commit.gpgsign=true` overrides a global false. The commit runs only for a picked run whose `Pick.Lane` is this lane. + +Reproduction: on git 2.39.5, set `commit.gpgsign=true` and `gpg.program` to a mode-0755 script in the checkout; start a picked run and let `abcd implement step` reach the record commit. The script runs with gpg's `--status-fd=2 -bsau` arguments, the pre-commit hook does not, and git exits 128 when the script writes no signature. `gpg.format=ssh` with `gpg.ssh.program`, and `gpg.ssh.defaultKeyCommand` with no signing key, each start too. `-c commit.gpgsign=false` before `commit` exits 0 and starts nothing. diff --git a/.abcd/work/issues/resolved/iss-2610090821527948-missing-promisor-object-triggers-configured-fetch.md b/.abcd/work/issues/resolved/iss-2610090821527948-missing-promisor-object-triggers-configured-fetch.md index 8cd0468bf..8c382584d 100644 --- a/.abcd/work/issues/resolved/iss-2610090821527948-missing-promisor-object-triggers-configured-fetch.md +++ b/.abcd/work/issues/resolved/iss-2610090821527948-missing-promisor-object-triggers-configured-fetch.md @@ -5,7 +5,7 @@ slug: "missing-promisor-object-triggers-configured-fetch" severity: "minor" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-4jpc-8vpr-5x2j, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/gitutil/repo.go" @@ -16,6 +16,8 @@ impact: fix abcd's git environment does not set `GIT_NO_LAZY_FETCH`, so an object read (`show`, `cat-file`, `ls-tree`, or `tag --list` with an object-reading `tag.sort`) of a missing object in a promisor checkout lazy-fetches and runs the repository's transport program (`remote.origin.uploadpack` on a local or file:// URL, `core.sshCommand` on ssh://). -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-4jpc-8vpr-5x2j (draft, severity medium). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `gitEnv` (internal/gitutil/repo.go:74) ends its config keys at core.fsmonitor (internal/gitutil/repo.go:107) and sets no GIT_NO_LAZY_FETCH; `Run`, `RunLimited` and `IsolatedEnv` (internal/gitutil/repo.go:122) share it. Sinks: `lifeboat.Plan` (internal/core/lifeboat/plan.go:187) calls `buildArchaeology` (:305), where `gvRemovedDependencies` shows a historical manifest and ignores the error (internal/core/lifeboat/graveyard_archaeology.go:242), so `abcd disembark plan` and `pack` succeed after the program ran; `committedRegistry` cat-files `HEAD:.abcd/guard.json` and falls back to defaults (internal/core/guard/config.go:99); `GitExistingTags` (internal/core/launch/retention.go:108) is reached by `abcd launch --dry-run` twice (internal/surface/cli/launch_deep.go:77 via `changelog.LatestReleaseTag`, internal/core/changelog/anchor.go:58, and internal/core/launch/dryrun.go:229), by changelog derive and guard, capture's deferral path, `reflect` `previousTag` (internal/core/reflect/seed.go:212) and the receipt gate's `releaseImpact` (internal/core/lint/lint.go:1437). `disembark probe` does not take this path. \ No newline at end of file +Evidence (lines at main 7549ca2d5): `gitEnv` (internal/gitutil/repo.go:74) ends its config keys at core.fsmonitor (internal/gitutil/repo.go:107) and sets no GIT_NO_LAZY_FETCH; `Run`, `RunLimited` and `IsolatedEnv` (internal/gitutil/repo.go:122) share it. Sinks: `lifeboat.Plan` (internal/core/lifeboat/plan.go:187) calls `buildArchaeology` (:305), where `gvRemovedDependencies` shows a historical manifest and ignores the error (internal/core/lifeboat/graveyard_archaeology.go:242), so `abcd disembark plan` and `pack` succeed after the program ran; `committedRegistry` cat-files `HEAD:.abcd/guard.json` and falls back to defaults (internal/core/guard/config.go:99); `GitExistingTags` (internal/core/launch/retention.go:108) is reached by `abcd launch --dry-run` twice (internal/surface/cli/launch_deep.go:77 via `changelog.LatestReleaseTag`, internal/core/changelog/anchor.go:58, and internal/core/launch/dryrun.go:229), by changelog derive and guard, capture's deferral path, `reflect` `previousTag` (internal/core/reflect/seed.go:212) and the receipt gate's `releaseImpact` (internal/core/lint/lint.go:1437). `disembark probe` does not take this path. + +Reproduction: on git 2.39.5, delete the loose blob of a historical manifest the plan shows; set `extensions.partialClone=origin`, `remote.origin.promisor=true`, a partial-clone filter, `remote.origin.url` a local path and `remote.origin.uploadpack` a mode-0755 script; run `abcd disembark plan`. The script runs and Plan returns a lifeboat with a nil error. For the tag sink, point `refs/tags/v1.1.0` at a deleted commit, set `tag.sort=taggerdate` and call `GitExistingTags`: the script runs and the call returns exit 128. `--sort=refname` and `-c tag.sort=refname` do not stop it; `GIT_NO_LAZY_FETCH=1` does. diff --git a/.abcd/work/issues/resolved/iss-2610090821531394-read-only-git-log-runs-repo-gpg-program.md b/.abcd/work/issues/resolved/iss-2610090821531394-read-only-git-log-runs-repo-gpg-program.md index 4e06922e3..1be67451f 100644 --- a/.abcd/work/issues/resolved/iss-2610090821531394-read-only-git-log-runs-repo-gpg-program.md +++ b/.abcd/work/issues/resolved/iss-2610090821531394-read-only-git-log-runs-repo-gpg-program.md @@ -5,7 +5,7 @@ slug: "read-only-git-log-runs-repo-gpg-program" severity: "major" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-9fp7-gmjr-v28j, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/gitutil/repo.go" @@ -16,6 +16,8 @@ impact: fix abcd's read-only `git log` and `git show` calls do not pin `log.showSignature=false`, so a checkout with `log.showSignature=true`, a repo `gpg.program` and one `gpgsig` commit runs that program as the operator during probes such as `abcd disembark probe`. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-9fp7-gmjr-v28j (draft, severity high). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): the comment at internal/gitutil/repo.go:22 names log as a command the hooks and fsmonitor pins make safe; `isolatedArgs` (internal/gitutil/repo.go:52) does not set log.showSignature. Sinks: `gitReverts` asks for `log --format=%s` (internal/core/lifeboat/sources_git.go:35) under `abcd disembark probe`; `LoadHistory` runs `git log --reverse --name-status` (internal/core/site/dates.go:63); `walkMentionCommits` (internal/core/capture/mentions.go:360); the decisions-append subject read (internal/core/lint/decisionsappend.go:308). A script that exits 0 (and in a replay one that exits 1) leaves the call returning the subject and a nil error. The directory must be a copy or zip including `.git`; a clone does not carry it. \ No newline at end of file +Evidence (lines at main 7549ca2d5): the comment at internal/gitutil/repo.go:22 names log as a command the hooks and fsmonitor pins make safe; `isolatedArgs` (internal/gitutil/repo.go:52) does not set log.showSignature. Sinks: `gitReverts` asks for `log --format=%s` (internal/core/lifeboat/sources_git.go:35) under `abcd disembark probe`; `LoadHistory` runs `git log --reverse --name-status` (internal/core/site/dates.go:63); `walkMentionCommits` (internal/core/capture/mentions.go:360); the decisions-append subject read (internal/core/lint/decisionsappend.go:308). A script that exits 0 (and in a replay one that exits 1) leaves the call returning the subject and a nil error. The directory must be a copy or zip including `.git`; a clone does not carry it. + +Reproduction: on git 2.39.5, put a mode-0755 script in the copied checkout, set `gpg.program` to it and `log.showSignature=true`, and point HEAD at a commit object carrying a `gpgsig` header. From another cwd run `abcd disembark probe` on it: the script runs and probe returns a report and nil. With unsigned history the script does not run. `rev-list`, `status`, `diff --name-only` and `cat-file -p` do not run it; `show --format=%s --no-patch` of the signed commit does. diff --git a/.abcd/work/issues/resolved/iss-2610090821531570-record-lint-agent-diff-runs-repo-diff-drivers.md b/.abcd/work/issues/resolved/iss-2610090821531570-record-lint-agent-diff-runs-repo-diff-drivers.md index 64aa0e179..964ed896c 100644 --- a/.abcd/work/issues/resolved/iss-2610090821531570-record-lint-agent-diff-runs-repo-diff-drivers.md +++ b/.abcd/work/issues/resolved/iss-2610090821531570-record-lint-agent-diff-runs-repo-diff-drivers.md @@ -5,7 +5,7 @@ slug: "record-lint-agent-diff-runs-repo-diff-drivers" severity: "minor" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-q333-3p8r-xc2f, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/core/lint/agentcontract.go" @@ -16,6 +16,8 @@ impact: fix record-lint's armed agent-diff check runs `git diff --unified=0` without `--no-ext-diff --no-textconv`, so a repository `diff.external`, `diff..textconv` or `diff..command` runs as the operator and its stdout can forge the `prompt_version` bump the check looks for. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-q333-3p8r-xc2f (draft, severity medium). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `promptVersionChanged` runs `gitutil.Run(repoRoot, "diff", "--unified=0", rangeSpec, "--", rel)` (internal/core/lint/agentcontract.go:388-389) and returns true on any added line prefixed `+prompt_version:`. The range is armed only by `record-lint -agent-diff` (cmd/record-lint/main.go:124, `lint.ArmAgentDiff`) or the Makefile record-lint target (`origin/main...HEAD`); default `abcd lint` does not arm it. The neighbouring decisions-append diff already passes `--no-ext-diff` and `--no-textconv` (internal/core/lint/decisionsappend.go:396, rationale at :378). A CI checkout of a pull request does not install the author's `.git/config`; a local run does. \ No newline at end of file +Evidence (lines at main 7549ca2d5): `promptVersionChanged` runs `gitutil.Run(repoRoot, "diff", "--unified=0", rangeSpec, "--", rel)` (internal/core/lint/agentcontract.go:388-389) and returns true on any added line prefixed `+prompt_version:`. The range is armed only by `record-lint -agent-diff` (cmd/record-lint/main.go:124, `lint.ArmAgentDiff`) or the Makefile record-lint target (`origin/main...HEAD`); default `abcd lint` does not arm it. The neighbouring decisions-append diff already passes `--no-ext-diff` and `--no-textconv` (internal/core/lint/decisionsappend.go:396, rationale at :378). A CI checkout of a pull request does not install the author's `.git/config`; a local run does. + +Reproduction: on git 2.39.5, in a local checkout, plant `refs/remotes/origin/main`, change a prompt path, set `diff.external` to a mode-0755 script, and run `go run ./cmd/record-lint -agent-diff origin/main...HEAD`. The script runs; if it prints `+prompt_version: 9.9.9` the unbumped-prompt finding is absent and `lint.Lint` returns nil. `* diff=evil` with `diff.evil.textconv` or `diff.evil.command` runs too. diff --git a/.abcd/work/issues/resolved/iss-2610090821543020-core-worktree-ancestor-becomes-store-root.md b/.abcd/work/issues/resolved/iss-2610090821543020-core-worktree-ancestor-becomes-store-root.md index fa1ad3b80..3e8d9e208 100644 --- a/.abcd/work/issues/resolved/iss-2610090821543020-core-worktree-ancestor-becomes-store-root.md +++ b/.abcd/work/issues/resolved/iss-2610090821543020-core-worktree-ancestor-becomes-store-root.md @@ -5,7 +5,7 @@ slug: "core-worktree-ancestor-becomes-store-root" severity: "minor" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-6wfm-pfhv-w267, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/gitutil/repo.go" @@ -16,6 +16,8 @@ impact: fix A copied checkout whose `.git/config` sets `core.worktree=../..` makes abcd take an ancestor directory as the repository root, so `abcd decide`, intent, spec, capture and memory writes land under that ancestor instead of the checkout. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-6wfm-pfhv-w267 (draft, severity medium). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `ToplevelContext` runs `rev-parse --show-toplevel` (internal/gitutil/repo.go:565) and `ToplevelShaped` accepts any absolute answer that `PathWithin` says contains the asked directory (internal/gitutil/repo.go:599, internal/fsutil/paths.go:125). `CheckoutRoot` (internal/gitutil/repo.go:369) feeds `decideStoreRoot` (internal/surface/cli/decide.go:91) and the intent, spec, memory and capture-ledger roots. internal/core/site/outdir.go:173 names `core.worktree` as a decoy; `site.Build` and `launch.PrecheckPayload` refuse the parent. The written bodies are abcd's own records, and no program runs. `git clone` drops the key; the config has to arrive in a copied `.git`. \ No newline at end of file +Evidence (lines at main 7549ca2d5): `ToplevelContext` runs `rev-parse --show-toplevel` (internal/gitutil/repo.go:565) and `ToplevelShaped` accepts any absolute answer that `PathWithin` says contains the asked directory (internal/gitutil/repo.go:599, internal/fsutil/paths.go:125). `CheckoutRoot` (internal/gitutil/repo.go:369) feeds `decideStoreRoot` (internal/surface/cli/decide.go:91) and the intent, spec, memory and capture-ledger roots. internal/core/site/outdir.go:173 names `core.worktree` as a decoy; `site.Build` and `launch.PrecheckPayload` refuse the parent. The written bodies are abcd's own records, and no program runs. `git clone` drops the key; the config has to arrive in a copied `.git`. + +Reproduction: on git 2.39.5, set `core.worktree=../..` in `/evil/.git/config`; from `evil` run `abcd decide ""`. The ADR is created under `/.abcd/development/decisions/adrs/`, not under `evil`; the same parent receives a spec, an intent, a capture issue and the memory pages. A sibling worktree that does not contain the checkout is refused with ErrToplevelShape. diff --git a/.abcd/work/issues/resolved/iss-2610090821548169-launch-dirty-check-runs-repo-clean-filter.md b/.abcd/work/issues/resolved/iss-2610090821548169-launch-dirty-check-runs-repo-clean-filter.md index 4b71c1f0e..192d0e0ad 100644 --- a/.abcd/work/issues/resolved/iss-2610090821548169-launch-dirty-check-runs-repo-clean-filter.md +++ b/.abcd/work/issues/resolved/iss-2610090821548169-launch-dirty-check-runs-repo-clean-filter.md @@ -5,7 +5,7 @@ slug: "launch-dirty-check-runs-repo-clean-filter" severity: "major" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-qv3j-fq4f-g5xr, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/core/launch/gates.go" @@ -16,6 +16,8 @@ impact: fix The launch dirty-tree check (`git diff ... HEAD` in `DirtyTreeFiles`) runs a repository `filter..clean` program as the operator on a copied checkout, and the gate can still report the tree clean. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-qv3j-fq4f-g5xr (draft, severity high). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `DirtyTreeFiles` (internal/core/launch/gates.go:920) runs `diff --no-renames --name-only -z HEAD` through `gitutil.Run` (internal/core/launch/gates.go:929), whose `isolatedArgs` pins only hooksPath, fsmonitor and quotePath (internal/gitutil/repo.go:52). A copied checkout loses the index stat, so git re-hashes the worktree through the clean filter. `dirtyTreeGate` reports `clean` on an empty list (internal/core/launch/gates.go:950); `abcd launch --dry-run` reaches it once `.abcd/config/artefact.json` loads (`{"kind":"binary"}` suffices), and the ship ingest reaches it when it stages a payload, `--allow-dirty` still running the diff. Siblings of the same command class: `DirtyPayloadFiles` (internal/core/launch/archive.go:490) and `dirtyCorpusPaths` (internal/core/intent/consistency.go:250). `--no-ext-diff` and `--no-textconv` do not stop a clean filter, and blanking only `.clean` leaves `.process`. \ No newline at end of file +Evidence (lines at main 7549ca2d5): `DirtyTreeFiles` (internal/core/launch/gates.go:920) runs `diff --no-renames --name-only -z HEAD` through `gitutil.Run` (internal/core/launch/gates.go:929), whose `isolatedArgs` pins only hooksPath, fsmonitor and quotePath (internal/gitutil/repo.go:52). A copied checkout loses the index stat, so git re-hashes the worktree through the clean filter. `dirtyTreeGate` reports `clean` on an empty list (internal/core/launch/gates.go:950); `abcd launch --dry-run` reaches it once `.abcd/config/artefact.json` loads (`{"kind":"binary"}` suffices), and the ship ingest reaches it when it stages a payload, `--allow-dirty` still running the diff. Siblings of the same command class: `DirtyPayloadFiles` (internal/core/launch/archive.go:490) and `dirtyCorpusPaths` (internal/core/intent/consistency.go:250). `--no-ext-diff` and `--no-textconv` do not stop a clean filter, and blanking only `.clean` leaves `.process`. + +Reproduction: on git 2.39.5, in a repo with `.abcd/config/artefact.json` = `{"kind":"binary"}`, point `filter.evil.clean` at a mode-0755 script that appends a mark and cats stdin, set `.git/info/attributes` to `* filter=evil`, copy the directory, and run `abcd launch --dry-run` in the copy from another cwd. The mark is written and the dirty-tree row says clean. Do not switch to `git status`: on a copied tree `git status --porcelain` also runs the filter. diff --git a/.abcd/work/issues/resolved/iss-2610090821552801-implement-land-stage-trusts-planted-worktree-path.md b/.abcd/work/issues/resolved/iss-2610090821552801-implement-land-stage-trusts-planted-worktree-path.md index ba9df1389..96e572ad7 100644 --- a/.abcd/work/issues/resolved/iss-2610090821552801-implement-land-stage-trusts-planted-worktree-path.md +++ b/.abcd/work/issues/resolved/iss-2610090821552801-implement-land-stage-trusts-planted-worktree-path.md @@ -5,7 +5,7 @@ slug: "implement-land-stage-trusts-planted-worktree-path" severity: "minor" category: "security" source: "agent-finding" -found_during: "private security report, filed 2026-10-05" +found_during: "private security advisory GHSA-qfpj-v8c5-c4fw, filed 2026-10-05" origin: researcher-authored production_mode: hand-written found_at: "internal/core/implement/loop/land.go" @@ -16,6 +16,8 @@ impact: fix The implement loop's land stage accepts any non-empty worktree path from the run state file, so a planted state makes `abcd implement step` create `/.abcd/.work.local` (mode 0700) in a directory the state names. -A private security report, fixed in this release; its advisory, with the full text and reproduction, is published with the release. +Private security advisory GHSA-qfpj-v8c5-c4fw (draft, severity low). Full text, evidence and reproduction: the security-drain-2026-10-09 run directory in the main checkout's local tier. This record stays uncommitted until its fix lands; the fix commit adds it directly to resolved/. -Evidence (lines at main 7549ca2d5): `briefStage` refuses a worktree that is not the path `laneWorktree` derives (internal/core/implement/loop/brief.go:104-105). `landStage` only checks the string is non-empty and skips `syncLane` once a landing is recorded (internal/core/implement/loop/land.go:131, :139). `landRecords` uses that worktree when `landing.closes` is true (internal/core/implement/loop/land.go:237) and calls `fsutil.EnsureRealDirAll` there (internal/core/implement/loop/land.go:269). The state file is gitignored, so it arrives in a zip, not a clone. The result is an empty 0700 directory tree; no file body is written and no hook in the other repository runs. \ No newline at end of file +Evidence (lines at main 7549ca2d5): `briefStage` refuses a worktree that is not the path `laneWorktree` derives (internal/core/implement/loop/brief.go:104-105). `landStage` only checks the string is non-empty and skips `syncLane` once a landing is recorded (internal/core/implement/loop/land.go:131, :139). `landRecords` uses that worktree when `landing.closes` is true (internal/core/implement/loop/land.go:237) and calls `fsutil.EnsureRealDirAll` there (internal/core/implement/loop/land.go:269). The state file is gitignored, so it arrives in a zip, not a clone. The result is an empty 0700 directory tree; no file body is written and no hook in the other repository runs. + +Reproduction: on git 2.39.5, hand-write the state under `.abcd/.work.local/run/` for a run whose one lane is at stage land, with full base and head SHAs, a real branch at that tip, `landing.closes` true and `worktree` a relative path to another existing git repository; run `abcd implement step`. `/.abcd/.work.local` exists mode 0700, then `intent.Reconcile` fails on the absent spec. A missing branch or a missing landing record returns before the mkdir. diff --git a/.abcd/work/issues/resolved/iss-2610090925390900-guard-allows-shellopts-ps4-substitution.md b/.abcd/work/issues/resolved/iss-2610090925390900-guard-allows-shellopts-ps4-substitution.md index 7ea15a497..13a2b60c9 100644 --- a/.abcd/work/issues/resolved/iss-2610090925390900-guard-allows-shellopts-ps4-substitution.md +++ b/.abcd/work/issues/resolved/iss-2610090925390900-guard-allows-shellopts-ps4-substitution.md @@ -14,4 +14,4 @@ resolution: "the guard judges the text a line hands bash through a prompt variab impact: fix --- -abcd guard allows a bash -c whose environment prefix sets SHELLOPTS=xtrace and a PS4 holding a command substitution: bash expands PS4 before tracing the first command, so a blocker in PS4 runs although the -c payload is harmless. Sibling of the BASH_ENV finding iss-2610090821484829, found in the security-drain-2026-10-09 sweep; kept uncommitted until its fix lands. +abcd guard allows a bash -c whose environment prefix sets SHELLOPTS=xtrace and a PS4 holding a command substitution: bash expands PS4 before tracing the first command, so a blocker in PS4 runs although the -c payload is harmless. Reproduced at main 016c1510a: `SHELLOPTS=xtrace PS4='$(pkill node)' bash -c true` (and the `env` form) judges allow while `pkill node` blocks. Sibling of the BASH_ENV finding iss-2610090821484829, found in the security-drain-2026-10-09 sweep; kept uncommitted until its fix lands. diff --git a/.abcd/work/issues/resolved/iss-2610090925399967-guard-allows-exported-bash-function-env.md b/.abcd/work/issues/resolved/iss-2610090925399967-guard-allows-exported-bash-function-env.md index dfc455dfd..c309d38db 100644 --- a/.abcd/work/issues/resolved/iss-2610090925399967-guard-allows-exported-bash-function-env.md +++ b/.abcd/work/issues/resolved/iss-2610090925399967-guard-allows-exported-bash-function-env.md @@ -15,4 +15,4 @@ resolution: "env and sudo now step every '=' operand before the command as an as impact: fix --- -abcd guard allows a bash -c whose environment carries an exported function (a BASH_FUNC_%% variable, through env): bash imports it at startup, so a function named like the -c command runs a blocker in its body although the -c payload is harmless. Sibling of the BASH_ENV finding iss-2610090821484829, found in the security-drain-2026-10-09 sweep; kept uncommitted until its fix lands. +abcd guard allows a bash -c whose environment carries an exported function (a BASH_FUNC_%% variable, through env): bash imports it at startup, so a function named like the -c command runs a blocker in its body although the -c payload is harmless. Reproduced at main 016c1510a: `env 'BASH_FUNC_true%%=() { pkill node; }' bash -c true` judges allow while `pkill node` blocks. Sibling of the BASH_ENV finding iss-2610090821484829, found in the security-drain-2026-10-09 sweep; kept uncommitted until its fix lands. diff --git a/.abcd/work/issues/resolved/iss-2610090932257904-guard-allows-shell-stdin-redirect-script.md b/.abcd/work/issues/resolved/iss-2610090932257904-guard-allows-shell-stdin-redirect-script.md index 4fbf5e65c..d8108866f 100644 --- a/.abcd/work/issues/resolved/iss-2610090932257904-guard-allows-shell-stdin-redirect-script.md +++ b/.abcd/work/issues/resolved/iss-2610090932257904-guard-allows-shell-stdin-redirect-script.md @@ -14,4 +14,4 @@ resolution: "A shell with no -c string and no script operand that a redirection impact: fix --- -abcd guard allows a shell that reads its script from a file through a stdin redirect (`bash < f`, `bash -s < f`), while the pipe form `cat f | bash` blocks under interpreter-reads-stream: stdinStream (internal/core/guard/tokenize.go:49-54) is set for a pipe, a here-document, a here-string and a pipe into a group, never for a < redirect, so shellReadsStream (payload.go:1871-1874) never sees the file as the script. Sibling of iss-2610090821484829, found in the security-drain-2026-10-09 sweep; kept uncommitted until its fix lands. +abcd guard allows a shell that reads its script from a file through a stdin redirect (`bash < f`, `bash -s < f`), while the pipe form `cat f | bash` blocks under interpreter-reads-stream: stdinStream (internal/core/guard/tokenize.go:49-54) is set for a pipe, a here-document, a here-string and a pipe into a group, never for a < redirect, so shellReadsStream (payload.go:1871-1874) never sees the file as the script. Reproduced at main 016c1510a. Sibling of iss-2610090821484829, found in the security-drain-2026-10-09 sweep; kept uncommitted until its fix lands.