Skip to content

fix(untrusted): neutralize fuzzy closing-tag variants in wrap_untrusted - #46

Open
viistaproducts wants to merge 1 commit into
afairai:mainfrom
viistaproducts:fix/wrap-untrusted-fuzzy-closing-tag
Open

fix(untrusted): neutralize fuzzy closing-tag variants in wrap_untrusted#46
viistaproducts wants to merge 1 commit into
afairai:mainfrom
viistaproducts:fix/wrap-untrusted-fuzzy-closing-tag

Conversation

@viistaproducts

Copy link
Copy Markdown

What

wrap_untrusted only escaped the exact byte string </event_content>. Whitespace- or case-varied closing tags slip through unescaped:

</event_content>    -> escaped ✓
</event_content >   -> NOT escaped ✗   (trailing space)
</EVENT_CONTENT>    -> NOT escaped ✗   (uppercase)
< / event_content > -> NOT escaped ✗   (spaces around slash)
</event_content\t>  -> NOT escaped ✗   (tab)

Since LLMs don't parse XML strictly, a fuzzy closing tag can still read as the delimiter's end to the model, letting an attacker's subsequent text be interpreted as instructions rather than data — the exact break-out the wrapper exists to prevent. The docstring also claimed an attacker "cannot inject a fake </event_content>", which overstated the guarantee.

Change

  • Replace the literal str.replace with a case-insensitive, whitespace-tolerant regex over closing-tag variants only (requires the slash). Being close-only preserves the existing re-wrap idempotency contract (an inner opening tag is never escaped), which test_wrap_untrusted_idempotent_in_content_does_not_corrupt pins.
  • Corrected the docstring to describe the actual (now stronger) guarantee.
  • Added test_wrap_untrusted_neutralizes_fuzzy_closing_tags covering the four variants above.

Defense-in-depth: the UNTRUSTED_CONTENT_DIRECTIVE still instructs the model to ignore embedded directives; this closes the delimiter-escape gap that sat behind it.

Tests

tests/test_untrusted.py (12 passed), plus the UNTRUSTED-referencing suites test_schema_evolver.py + test_self_improvement_hardening.py (62 passed total). ruff check, ruff format --check, and mypy all clean.

🤖 Generated with Claude Code

wrap_untrusted only escaped the exact byte string `</event_content>`, so an
attacker could break out of the tagged data region with a whitespace- or
case-varied closing tag (`</event_content >`, `</EVENT_CONTENT>`,
`< / event_content >`, tab-separated). LLMs do not parse XML strictly, so a
fuzzy closing tag can still read as the delimiter's end, letting subsequent
text be interpreted as instructions.

Replace the literal str.replace with a case-insensitive, whitespace-tolerant
regex over closing-tag variants only (requires the slash), preserving the
re-wrap idempotency contract for inner opening tags. Add tests pinning the
fuzzy variants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant