Fencing untrusted text: look-alike closing markers survive inside the fence #12
Labels
No labels
bug
documentation
enhancement
security
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo-mcp#12
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo-mcp at
0b29e70b9, running the marker pattern against variant spellings.Related to #3.
The fence markers are fixed strings, and
_MARKERremoves only their exact ASCII spelling (allowing for spaces and case). A closer spelt with an invisible or look-alike character stays inside the fenced text, so a model can see what looks like the end of the fence followed by text outside it. The README says flatly that "Text inside cannot close its own fence".src/postulo_mcp/server.py:43-45(FENCE_OPEN,FENCE_CLOSE,_MARKER = re.compile(r"<<<\s*(?:end\s+)?untrusted[^>]*>>>", re.IGNORECASE)),src/postulo_mcp/server.py:385-388(fence),tests/test_server.py:263-268,README.md:100-101._MARKERremoves<<<end untrusted>>>and<<< END UNTRUSTED >>>, but keeps<<<end+U+200B+untrusted>>>(zero-width space), the same with U+200B after<<<,<<<end un+U+00AD+trusted>>>(soft hyphen),<<<end_untrusted>>>,<<<end-untrusted>>>and the fullwidth<<<end untrusted>>>. Any of them, on its own line inside an email body or a captured description, reaches the model as an apparent closer.--writeand the model treats the look-alike as a real boundary.<<<untrusted description 3f9c…>>>…<<<end untrusted 3f9c…>>>) and name the form inINSTRUCTIONS, so text written before the reply cannot know it. Before matching, NFKC-normalise and drop format characters (Unicode categoryCf), and widen the pattern to[\s_-]between the words. Correct the README sentence to what the code guarantees.Done when
tests/test_server.pyfences each of the variants above and finds exactly one closing marker, the server's own, in every result.