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
|
|
@ -69,11 +69,17 @@ class DispositionResult:
|
|||
|
||||
|
||||
# Invisible carriers have no legitimate place in a reference file: they block in
|
||||
# any tier regardless of trust or provenance (BRIEF §4.7).
|
||||
# any tier regardless of trust or provenance (BRIEF §4.7). Both the input-side
|
||||
# (``sanitize:*``, which strips) and the output-side presence labels are listed
|
||||
# so the invariant holds on the persist gate too — model output is never
|
||||
# sanitized, so its carrier signal arrives via ``output``/``lexicon`` instead.
|
||||
_CARRIER_LABELS = frozenset({
|
||||
"sanitize:zero-width",
|
||||
"sanitize:bidi-override",
|
||||
"sanitize:unicode-tag",
|
||||
"output:zero-width-present",
|
||||
"output:bidi-present",
|
||||
"lexicon:unicode-tags-present",
|
||||
})
|
||||
|
||||
_DISPOSITION_RANK = {
|
||||
|
|
@ -197,18 +203,20 @@ def guard(
|
|||
"""Run ``scan_fn`` and dispose the result, failing *closed* on error.
|
||||
|
||||
A scanner that raises (a missing detector dependency, a crash on crafted
|
||||
input) must never yield an auto-persist: an un-scannable artifact is a BLOCK
|
||||
(BRIEF §4.6, fail-closed).
|
||||
input) — or a disposition that raises (a malformed report) — must never
|
||||
yield an auto-persist: an un-scannable / un-disposable artifact is a BLOCK
|
||||
(BRIEF §4.6, fail-closed). ``decide`` runs inside the guarded block so the
|
||||
fail-closed guarantee is total.
|
||||
"""
|
||||
try:
|
||||
report = scan_fn()
|
||||
except Exception as exc: # noqa: BLE001 — fail closed on ANY scanner error
|
||||
return decide(report, policy, provenance=provenance, transform_failed=transform_failed)
|
||||
except Exception as exc: # noqa: BLE001 — fail closed on ANY scan/dispose error
|
||||
return DispositionResult(
|
||||
Disposition.FAIL_SECURE,
|
||||
(f"fail-closed: scanner error: {type(exc).__name__}",),
|
||||
(f"fail-closed: scan/dispose error: {type(exc).__name__}",),
|
||||
None,
|
||||
)
|
||||
return decide(report, policy, provenance=provenance, transform_failed=transform_failed)
|
||||
|
||||
|
||||
PRESET_TRUSTED_SOURCE = Policy(trust=Trust.TRUSTED)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue