Skip to content

The fence could be closed from inside: untrusted text is neutralised … - #4

Merged
leighanne77 merged 1 commit into
mainfrom
fix/user-data-fence
Sep 28, 2026
Merged

leighanne77 merged 1 commit into
mainfrom
fix/user-data-fence

Conversation

@leighanne77

Copy link
Copy Markdown
Owner

…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.

…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>
@leighanne77
leighanne77 merged commit 7cf1d42 into main Sep 28, 2026
2 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.

1 participant