Skip to content

Stop secret-shape detection from swallowing file paths - #85

Merged
steflsd merged 1 commit into
mainfrom
fix/secret-shapes-path-false-positive
Jul 27, 2026
Merged

Stop secret-shape detection from swallowing file paths#85
steflsd merged 1 commit into
mainfrom
fix/secret-shapes-path-false-positive

Conversation

@steflsd

@steflsd steflsd commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai ignore

The secret-json-value pattern matches KEY\n VALUE with no separator required between key and value. So any line ending in a token containing a secret-ish word (token, secret, auth, password) — as long as the word isn't at position 0 — caused the entire next whitespace-delimited token to be registered as a secret.

In practice that ate file paths out of tool output.

Impact

Any git diff --name-only, ls, find, or grep listing in a repo containing an *Auth*/*Token*-named file lost paths. Measured across 10 repos: 27 of 4,265 tracked paths (~0.6%).

Two properties made it hard to spot:

  • Silent. The path is replaced by a FICTA_<hex> surrogate, not dropped, so a listing looks well-formed.
  • Position-dependent. It depends on the preceding line, so the same path survives one listing and vanishes from the next. Once registered, it stays redacted for the proxy's lifetime.
Fires Doesn't fire
convex/lib/rotateToken → next path packages/token (word at position 0)
apps/web/hooks/useAuth.ts → next path packages/authStore (same reason)
comment ending (registered-secretpath:12: bare auth
prose ending oauth → next path paths with no secret-ish line above

No leak, and no request failure — restore-into-tools puts the real path back into later tool args. The cost is that an agent sees an opaque token where a path should be and reasons about it. This was found because a session flagged one as evidence that "something is injecting into git config" and spent a turn on a phantom bug.

Fix

Reject values shaped like multi-segment filesystem paths, tolerating a trailing grep/ripgrep locator (src/defaults.ts:12:9). Ordered after the credential-URL and known-shape checks so those still win.

A segment that looks like credential material — ≥20 chars, no ./-/_, mixing letter cases with digits — disqualifies the whole value, so a slash-containing secret isn't mistaken for a path.

Length alone does not work as the discriminator. A 32-char segment cap still ate src/routes/api/v1/$.ts and a 33-char filename; widening to 64 then dropped a genuine credential (Xk9s…uI4o/S, 40-char segment). Requiring an unbroken mixed-case-plus-digit run keeps content-hashed assets like app.4f3a2b1c9d8e.js on the path side, since they carry separators.

Relationship to #83

This is the text-surface sibling of the JSON-key corruption fixed there. That fix separates leaves in the \n-joined detection text; this one fires on genuine multi-line string leaves, so it survived. Verified present in published 0.2.1 by running the repro against the installed tarball — which is what shimmed sessions actually execute.

Verification

  • 27 → 0 false positives across the same 4,265 files.
  • Regression tests: five listing shapes must register zero values (incl. a path:line: locator and src/routes/api/v1/$.ts), plus a true-positive guard that base64 and slash-containing credentials still pair with a bare API_TOKEN key — that second test is what caught the 64-char-cap mistake.
  • pnpm check (biome + engine-boundary + typecheck + tests): 51 files, 562 tests, all pass.

🤖 Generated with Claude Code

The `secret-json-value` pattern matches `KEY\n VALUE` with no separator
required between the two, so any line ending in a token containing a
secret-ish word (`token`, `secret`, `auth`, `password`) not at position 0
caused the entire next whitespace-delimited token to be registered as a
secret.

In practice that ate file paths out of tool output: `git diff --name-only`,
`ls`, `find`, and grep listings in any repo with an `*Auth*`/`*Token*`-named
file lost roughly one path per 150. Measured across 10 repos, 27 of 4,265
tracked paths were affected. The loss is silent and position-dependent — it
turns on the preceding line, so the same path survives one listing and
vanishes from the next — and once registered the value stays redacted for
the proxy's lifetime.

Reject values shaped like multi-segment filesystem paths, tolerating a
trailing grep/ripgrep locator (`src/defaults.ts:12:9`). The check is ordered
after the credential-URL and known-shape checks so those still win, and a
segment that looks like credential material (>=20 chars, no separator,
mixing letter cases with digits) disqualifies the whole value so a
slash-containing secret is not mistaken for a path. Length alone does not
work as the discriminator: capping segment length either keeps eating real
filenames or drops genuine credentials.

This is the text-surface sibling of the JSON-key corruption fixed in #83.
That fix separates leaves in the joined detection text; this one fires on
genuine multi-line string leaves, so it survived.

Measured 27 -> 0 false positives across the same 4,265 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@steflsd
steflsd merged commit 2905b23 into main Jul 27, 2026
4 checks passed
@steflsd
steflsd deleted the fix/secret-shapes-path-false-positive branch July 27, 2026 06:14
@github-actions github-actions Bot mentioned this pull request Jul 27, 2026
steflsd added a commit that referenced this pull request Jul 27, 2026
The detector docs described what secret-shapes catches but not what it
deliberately does not, which invites the assumption that enabling it covers
any secret in any config an agent reads.

Add a "Known coverage limits" section covering the three key/value pairing
positions where an opaque value is missed (secret-ish word at the start of
the key, decoration between separator and value, flag forms with no
separator), each with the reason it is not simply widened. Also document the
path-shaped-value rejection added in #85, so the behaviour reads as
intentional rather than as a detector miss.

Cross-reference from the threat model's "Intentionally not covered" list,
and restate that none of this affects the exact-match promise for registered
values.

Co-authored-by: Stefan Lesicnik <steflsd@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant