Skip to content

Add SecretPatternFilter — block real credential formats, not just keywords - #73

Merged
higagan merged 1 commit into
mainfrom
feat/secret-pattern-filter
Aug 8, 2026
Merged

Add SecretPatternFilter — block real credential formats, not just keywords#73
higagan merged 1 commit into
mainfrom
feat/secret-pattern-filter

Conversation

@higagan

@higagan higagan commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary

SensitiveDataFilter matches the literal words secret, password, and api_key. That means a live sk-ant-… or AKIA… key passed straight through the bundled default, while ordinary prose containing "password" got blocked. Credential exfiltration is the most common reason someone reaches for @shield_tool, and it was the case the default didn't cover.

SecretPatternFilter matches the shape of a credential rather than the vocabulary around it:

engine = PolicyEngine([
    URLAllowList(allowed_domains=["api.mycompany.com"]),
    SecretPatternFilter(),
])

http_post("https://api.mycompany.com/v1", "AKIAIOSFODNN7EXAMPLE")
# ModelFuzzBlockError: String contains a possible AWS access key ID

Covers Anthropic, OpenAI, Stripe, AWS, GitHub (classic + fine-grained), Google, and Slack key formats, JWTs, and PEM private-key headers. Extend with extra_patterns={...}, or replace the table with patterns={...}.

Opt-in — the bare @shield_tool default is unchanged, so this is not a breaking change (as scoped in #70).

Two things worth a reviewer's attention

  • The block reason never quotes the matched text. Blocks are logged at WARNING with the reason attached; a reason echoing the key would leak the exact thing the rule exists to contain. It names the format and where it was found, nothing else. There's a test pinning this.
  • \b anchors on the sk- patterns. Without it, sk-[A-Za-z0-9_-]{20,} matches inside ordinary hyphenated words — task-oriented-approach-for-agents and risk-management-… both tripped as OpenAI keys during development. Caught before commit; regression tests added.

Also extracts the container walk shared with SensitiveDataFilter into _iter_strings so the two can't drift. Behavior of SensitiveDataFilter is unchanged (its existing tests all pass untouched).

Limits are documented rather than papered over, in the same spirit as the rest of the README: listed formats only, and it matches shape rather than validity — a placeholder or revoked key in the right shape blocks like a live one.

Fixes #70

Test plan

  • uv run ruff check . / uv run ruff format --check . / uv run mypy --strict src/modelfuzz all clean
  • uv run pytest -q155 passed (43 new), covering: every bundled format; the motivating gap in both directions (a real key SensitiveDataFilter misses; prose it blocks that this one correctly allows); specific-beats-broad ordering (sk-ant-… reports as Anthropic, not OpenAI); the reason never echoing the secret; false-positive guards for sk- inside hyphenated words; the full container walk (nested dicts, dict keys, list/tuple/set/frozenset, bytes/bytearray, cycles); the documented custom-object limit; and all three config modes including an explicitly empty patterns={}
  • Ran the README snippet end-to-end — benign call passes, blocked call raises with exactly the documented message
  • Docs updated: new "Blocking real credentials" README section, two Limitations entries, and AGENTS.md rule 4 (assistants previously had a standing instruction that ModelFuzz cannot detect credentials — now corrected, with the caveats to pass on)

Note: CI may be red or stuck on runner acquisition — GitHub is mid-outage on Actions. Everything above was verified locally.

The bundled SensitiveDataFilter matches the literal words "secret",
"password" and "api_key", so a live sk-... or AKIA... key passed
straight through while ordinary prose about a password was blocked.
Someone wrapping @shield_tool for credential exfiltration -- the most
common reason to reach for this -- was not protected against it.

SecretPatternFilter matches the shape of a credential instead of the
vocabulary around it: Anthropic, OpenAI, Stripe, AWS, GitHub, Google
and Slack key formats, JWTs, and PEM private-key headers. It is opt-in,
so the bare @shield_tool default is unchanged.

The block reason names the format and never quotes the matched text.
Blocks are logged at WARNING, and a reason carrying the credential
would leak the thing the rule exists to contain.

Extracts the container walk shared with SensitiveDataFilter into
_iter_strings so the two cannot drift.

Fixes #70
@higagan
higagan merged commit f5a6d6d into main Aug 8, 2026
5 checks passed
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.

Ship a real credential-pattern filter (SecretPatternFilter) — default SensitiveDataFilter gives false sense of security

1 participant