Stop a mask's own edge from exposing half of a copy (#188) - #198
Merged
icebergai-review-bot[bot] merged 1 commit intoAug 19, 2026
Merged
Conversation
A redacted snippet could carry a fragment of the very secret it was masking. `_context` scrubs *whole* copies of a matched secret out of the text it emits, so a stray copy sitting between two masks was already covered. A copy that ran into the *side* of a mask was not: the mask cut it in two, the scrub never saw it whole, and the part sticking out was emitted verbatim — into the stored snippet, the console, notification payloads and hand-over payloads. Two ways in: * a neighbouring match whose mask begins mid-copy, and * the target's own mask, where a periodic secret overlaps itself and the copy starting inside the match runs out the far side. Both reproduce with ten contiguous characters of a twenty-character token. The existing defences only ever looked at the two window edges — `_clip_to_ secret_boundaries` pulls those back past a straddling copy and `_assert_no_bisected_secret` refuses the snippet if it failed. Interior seams had neither, though they emit context just the same. So: any copy overlapping a mask is now masked in full, the part sticking out getting a mask of its own rather than widening the mask it touches — the target's mask reports its own length and may reveal a structural prefix, and neither would still be true of a wider span. The bisection check now guards every edge that emits context, not just the window's two, and both it and the renderer read the same `_layout`, so the check cannot come to disagree with what was actually emitted. Costs nothing in dropped findings: over 40k adversarial inputs (small alphabets, so copies and self-overlap are dense) the fail-closed rate is identical to before — 1173 of 40000 — while straddle leaks go from widespread to none. The pathological cases that used to leak are now masked, not refused. `_occurrences` bounds both ends of its search; without a stop the final `find` scans the rest of the unit to prove there is nothing more, which on a large document is a whole pass per secret per snippet. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012sohE85sRDt6t2w3936rGJ
There was a problem hiding this comment.
Verdict
APPROVE
Completed bounded review across 1 immutable scope(s). No actionable findings in the supplied immutable delta.
Scope health
Convergence: healthy. Review mode: initial.
Recommended action: CONTINUE_INCREMENTAL.
- No escalation signals.
Prior findings
| Finding | Status |
|---|---|
| — | No prior finding state |
New findings
No new findings.
Fix-induced regressions
- None evidenced.
Uncertainty
- No material uncertainty recorded.
Validation
- Reviewed the supplied redaction-plan, partial-copy coverage, layout, and boundary-check changes.
- Exact-head CI passed per supplied review context.
Residual risks
- None identified.
icebergai-review-bot
Bot
deleted the
claude/codebase-review-cleanup-ovlli4
branch
August 19, 2026 22:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #188.
A redacted snippet could carry a fragment of the very secret it was masking — a violation of the invariant that only a masked snippet and a salted hash ever leave the engine (ADR 0004, CLAUDE.md invariant 2).
The defect
_contextscrubs whole copies of a matched secret out of the text it emits, so a stray copy sitting between two masks was already covered. A copy that ran into the side of a mask was not: the mask cut it in two, the scrub never saw it whole, and the part sticking out was emitted verbatim — into the stored snippet, the console, notification payloads and hand-over payloads.Two ways in, both reproduced against the merged code before fixing:
Ten contiguous characters of a twenty-character token in each case.
The existing defences only ever looked at the two window edges:
_clip_to_secret_boundariespulls those back past a straddling copy, and_assert_no_bisected_secretrefuses the snippet if that failed. Interior seams had neither, though they emit context just the same.The fix
_layout, so the check cannot come to disagree with what was actually emitted.Validation
_occurrencesbounds both ends of its search. Without a stop, the finalfindscans the rest of the unit to prove there is nothing more — a whole pass per secret per snippet on a large document. Benchmarked on an 85k-char unit with 200 matches: ~40µs added per snippet.ruff/ruff format/mypy/make docs-checkclean; full suite 1940 passed, 2 skipped (the two permission-bit tests, correctly skipped as root in this container; CI runs them).Operator note
Snippets already in the database were written by the old engine and are not rewritten in place. Ingest refreshes a finding's snippet on every re-sighting (
engines/ingest.py— "display metadata is refreshed"), so a full scan on engines carrying this fix replaces the stored snippet of every finding that still exists. A finding whose secret has since been removed keeps the snippet it was stored with. This is spelled out under Security in the CHANGELOG.Not included: the related nit in #188 about the mask marker disclosing a short secret's exact length (
hunter2→[7 chars redacted]). That changes snippet format everywhere and is a visible-output decision worth taking on its own rather than bundling into a security fix — #188 can stay open for it, or it can move to its own issue.🤖 Generated with Claude Code
https://claude.ai/code/session_012sohE85sRDt6t2w3936rGJ
Generated by Claude Code