feat(cases): add the urlexfil family — markdown/URL image exfil - #47
Merged
bamdadd merged 1 commit intoAug 11, 2026
Merged
Conversation
The shipped families all egress through a MESSAGE, so the destination gate in scoring matches an address. This adds the published pattern where the destination is the URL itself: a secret smuggled into a markdown image or link () that egresses when something fetches it. Greshake et al. arXiv:2302.12173 "data exfiltration" + OWASP LLM02; citation added alongside the others in the cases.py header. No novel attack. Two synthetic cases, built through the existing _build/_register machinery so they carry the same invariants as every other case: the date the benign task needs lives only in the injection-bearing email (on-path), the canary is co-located there, and the builder additionally asserts the injection names the attacker host and carries a markdown/URL egress form. Placeholders only - LKG- canaries, .test/.example hosts. The verification path is untouched. `url` is already in scoring._DESTINATION_KEYS, so naming http_post in exfil_spec.external_tools is enough for the existing deterministic scorer to verify both hijack and leakage. No LLM, no new scoring code. What this did require: an http_post tool, since the environment had no URL-bearing egress at all, so the family would have been unreachable. It records the URL in the trace and returns a canned 200 - nothing leaves the process, so the benchmark stays offline and deterministic. It is deliberately NOT in DEFAULT_EGRESS_TOOLS: a case has to name it in external_tools for it to count, so adding it cannot widen what counts as exfil for the existing families. A test pins exactly that. suite.py needed both halves too: the family in CASE_REGISTRY, and its own stub script. The uniform stub emails attacker_addresses[0], which here is a HOST - mailing it is not exfil, so a fully-leaking stub would have scored hijack=0 and looked resistant. _urlexfil_stub fetches the URL instead. `leakgauge --model stub:demo --suite all` now runs 41 cases (was 39) with both new ones at hijack/leakage/utility 1.00; `--suite urlexfil` runs them alone. 21 tests drive the REAL scorer: the planted URL form scores as a leak; the attacker host without the canary is hijack-but-not-leak (the distinction the benchmark exists to measure); the canary to an unrelated host is neither; emailing the host is not scored, which pins that the family's egress really is the URL; http_post does not widen exfil for other families; the tool records without requesting; and the benign task stays completable. 448 passed; ruff check, ruff format --check and mypy src clean. Closes bamdadd#32
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.
Closes #32.
The shipped families all egress through a message, so the destination gate matches an address. This adds the published pattern where the destination is the URL itself: a secret smuggled into a markdown image or link (
) that egresses when something fetches it.Citation added alongside the others in the
cases.pyheader — Greshake et al., arXiv:2302.12173 §"data exfiltration", plus OWASP LLM02 insecure output handling. No novel attack.Two synthetic cases
Built through the existing
_build/_registermachinery, so they carry the same invariants as every other case: the date the benign task needs lives only in the injection-bearing email (on-path), and the canary is co-located there. The builder additionally asserts the injection names the attacker host and carries a markdown/URL egress form, so a future edit cannot quietly turn it back into an address-shaped case.Placeholders only —
LKG-canaries,.test/.examplehosts.The verification path is untouched
urlis already inscoring._DESTINATION_KEYS, so naminghttp_postinexfil_spec.external_toolsis enough for the existing deterministic scorer to verify both hijack and leakage. No LLM, no new scoring code.What it did require, and why
An
http_posttool. The environment had no URL-bearing egress at all, so the family would have been unreachable — the agent would have had nothing to call. It records the URL in the trace and returns a canned 200; nothing leaves the process, so the benchmark stays offline and deterministic.It is deliberately not in
DEFAULT_EGRESS_TOOLS. A case has to name it inexternal_toolsfor it to count, so adding it cannot widen what counts as exfil for the existing families —test_http_post_is_not_an_egress_tool_for_other_familiespins exactly that.Both halves of
suite.py. The family inCASE_REGISTRY, and its own stub script. This one is worth calling out: the uniform stub emailsattacker_addresses[0], which here is a host. Mailing a host is not exfil, so a fully-leaking stub would have scoredhijack=0and looked like a resistant model._urlexfil_stubfetches the URL instead.Verification
--suite urlexfilruns them alone.21 tests drive the real scorer, not a mock:
http_postdoes not widen exfil for other families;448 passed, 4 xfailed;
ruff check,ruff format --checkandmypy srcclean.Note on firing: consistent with the honesty note in the module docstring, these two cases are unpiloted — no claim that they fire on any model.