fix(security): harden 5 adversarial-review findings (M1/M2/M3 + m4/m6) via TDD
Pre-release hardening from an independent adversarial review; each fixed test-first (failing test -> fix -> green). 214 tests pass. - entropy (M1): decode-and-rescan now runs BEFORE false-positive suppression, so an SRI/media-prefixed injection blob is still decoded and lexicon-rescanned. Suppression gates only the entropy finding, never the decode. - output/disposition (M3): the invisible-carrier invariant now holds on the persist gate. scan_output flags zero-width/BIDI presence and disposition treats those + lexicon:unicode-tags-present as any-tier carriers, so a carrier in model output fails secure even under a trusted policy. - contract (M2): assert_credential_allowlist catches a bare <PROVIDER>_KEY (e.g. STRIPE_KEY) that the old regex silently missed (fail-open). Deliberately broad: also flags PARTITION_KEY/SORT_KEY as loud, allowlistable FPs -- fail-loud beats fail-silent for an isolation control. - disposition (m6): guard runs decide inside its guarded block -> total fail-closed even on a malformed report. - output (m4): egress placeholder suppression anchors word markers (example, todo, ...) to a word boundary, closing a fail-open where a real secret merely containing such a word was suppressed. Docs: CHANGELOG Security subsection; README honest-limit for lexicon dedup (m5, documented tradeoff, not fixed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyRCQMocjZ6SmSQ6JidJ2k
This commit is contained in:
parent
86726ed109
commit
5397ba15a1
10 changed files with 233 additions and 18 deletions
|
|
@ -49,6 +49,11 @@ def test_critical_fails_secure_even_in_trusted_prose():
|
|||
("sanitize:zero-width", Severity.HIGH),
|
||||
("sanitize:bidi-override", Severity.HIGH),
|
||||
("sanitize:unicode-tag", Severity.CRITICAL),
|
||||
# M3: the same invariant must hold for the OUTPUT-gate carrier labels, so a
|
||||
# carrier surfacing in model output blocks in any tier too.
|
||||
("lexicon:unicode-tags-present", Severity.HIGH),
|
||||
("output:zero-width-present", Severity.HIGH),
|
||||
("output:bidi-present", Severity.HIGH),
|
||||
])
|
||||
def test_invisible_carrier_fails_secure_in_any_tier(carrier_label, severity):
|
||||
# zero-width/bidi are HIGH (would only WARN in trusted prose by severity
|
||||
|
|
@ -173,6 +178,14 @@ def test_guard_fails_secure_on_scanner_exception():
|
|||
assert result.max_severity is None
|
||||
|
||||
|
||||
def test_guard_fails_secure_when_decide_itself_raises():
|
||||
# m6 — total fail-closed: even if decide raises (e.g. a scan_fn that returns
|
||||
# a non-Report), guard yields FAIL_SECURE, never a leaked exception / persist.
|
||||
result = guard(lambda: None, TRUSTED) # None has no .max_severity() -> decide raises
|
||||
assert result.disposition is Disposition.FAIL_SECURE
|
||||
assert any("fail-closed" in reason for reason in result.reasons)
|
||||
|
||||
|
||||
def test_guard_passes_through_clean_scan():
|
||||
assert guard(lambda: _report(), TRUSTED).disposition is Disposition.WARN
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue