Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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).
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.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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).
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.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -16,6 +16,8 @@ impact: fix

`abcd guard` allows `BASH_ENV=<file> 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.
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.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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).
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.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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`.
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<home segments joined the same way> 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.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
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.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
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.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
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.
Loading
Loading