Repository navigation
The fence could be closed from inside: untrusted text is neutralised … - #4
Merged
Merged
Conversation
…first `_wrap_user_text` wrapped untrusted text as OPEN + text + CLOSE with no escaping, so any text containing `</USER_DATA>` ended the fence early and everything after it reached the model as conversation, not data. All three sites inherited it: history, the current message, and every tool result. The tool results are the dangerous one, because they carry contact notes and whatever the web lanes fetched: text nobody here wrote. JSON did not help. `json.dumps` escapes quotes and leaves angle brackets alone. The fix decides by normalising, not by matching spellings. A bracket pair (ASCII or a look-alike) whose content, with everything but letters and digits removed and casefolded, is `userdata` loses its brackets to plain parentheses. That one rule covers what a pattern keeps missing: whitespace or an escaped newline inside the tag, a zero-width character, a doubled slash, a self-closing form, a missing underscore, fullwidth and CJK brackets. The scan has a bounded interior and no adjacent quantifiers, so it stays linear; the obvious pattern backtracks quadratically on a long run of spaces. Ordinary angle brackets in notes are left as written. Tool results are neutralised value by value BEFORE serialising, since `json.dumps` turns a newline inside a tag into backslash-n, and once more after, for what `default=str` produces. And the history now has a total ceiling, `chat_history_max_chars` (40,000), checked before any work: the per-message and per-turn caps multiply to 400,000 characters of untrusted text. Tests first, red before the fix (37 failed): every spelling above on the message path and the tool-result path, asserted independently of the implementation (no ASCII bracket may survive inside the fence); the real route, with a note returned by search_contacts fenced exactly once; the ceiling refusing before the model is called; and a timing guard. Two forward guards pass on the old code by construction. P-05 states both requirements and the rulebook moves to version 2 (CLAUDE.md §3). CLAUDE.md §6 says it too, and the prompt-injection evals gain a closing-tag case. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
…first
_wrap_user_textwrapped untrusted text as OPEN + text + CLOSE with no escaping, so any text containing</USER_DATA>ended the fence early and everything after it reached the model as conversation, not data. All three sites inherited it: history, the current message, and every tool result. The tool results are the dangerous one, because they carry contact notes and whatever the web lanes fetched: text nobody here wrote. JSON did not help.json.dumpsescapes quotes and leaves angle brackets alone.The fix decides by normalising, not by matching spellings. A bracket pair (ASCII or a look-alike) whose content, with everything but letters and digits removed and casefolded, is
userdataloses its brackets to plain parentheses. That one rule covers what a pattern keeps missing: whitespace or an escaped newline inside the tag, a zero-width character, a doubled slash, a self-closing form, a missing underscore, fullwidth and CJK brackets. The scan has a bounded interior and no adjacent quantifiers, so it stays linear; the obvious pattern backtracks quadratically on a long run of spaces. Ordinary angle brackets in notes are left as written.Tool results are neutralised value by value BEFORE serialising, since
json.dumpsturns a newline inside a tag into backslash-n, and once more after, for whatdefault=strproduces.And the history now has a total ceiling,
chat_history_max_chars(40,000), checked before any work: the per-message and per-turn caps multiply to 400,000 characters of untrusted text.Tests first, red before the fix (37 failed): every spelling above on the message path and the tool-result path, asserted independently of the implementation (no ASCII bracket may survive inside the fence); the real route, with a note returned by search_contacts fenced exactly once; the ceiling refusing before the model is called; and a timing guard. Two forward guards pass on the old code by construction.
P-05 states both requirements and the rulebook moves to version 2 (CLAUDE.md §3). CLAUDE.md §6 says it too, and the prompt-injection evals gain a closing-tag case.