feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1)#162
Conversation
One shared "sanitize untrusted text" primitive: fences untrusted body text with a nonce-carrying BEGIN/END marker (anti-spoof — literal marker phrase is neutralized in the body first, so untrusted text can't forge a closing fence), and mechanically neutralizes @mentions, issue-closing keywords (fixes/closes/resolves #N), HTML angle brackets, control/ANSI escapes, and enforces a length cap. Pure bash + coreutils, offline, exit 0 always. NOT YET WIRED into any live loop/driver path — this is only the shared implementation issue #94 calls for; wiring a caller is a follow-up slice. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e spoof (issue #94) The anti-spoof marker-phrase neutralization matched only a single literal ASCII space between "untrusted"/"user"/"content". An attacker who forges a closing fence with a double space, tab, or other whitespace run between the words — even with the correct nonce (forceable via SANITIZE_NONCE or observable across turns) — produced a forged END fence that survived un-neutralized mid-body, plausibly readable by a whitespace-insensitive LLM consumer as an early close of the untrusted section. Make the marker-phrase regex whitespace-tolerant ([[:space:]]+ between each word, case-insensitive) so double-space/tab/mixed-whitespace variants are neutralized the same as the single-space case. The anti-spoof property no longer relies on nonce secrecy alone. Tests: add real-nonce double-space and tab forged-fence fixtures to test 6, asserting no whitespace-tolerant match of the closing fence survives except the one genuine trailing fence (verified: fails against the pre-fix regex, passes after). Add a tab-variant fixture for the fixes/closes/resolves #N keyword neutralization (already whitespace-tolerant, now covered). Remove the vacuous "output still ends with the real closing fence" assertion, which was structurally always true and proved nothing about breakout resistance.
…ible chars to close fence-spoof class (issue #94) The previous whitespace-tolerant marker-phrase neutralization used ASCII-only [[:space:]], so a Unicode space (e.g. NBSP U+00A0, NARROW NBSP U+202F) or a zero-width character (e.g. ZWSP U+200B) between/inside the marker words survived unneutralized under the C.UTF-8 locale the test gate runs in, forging a visually-identical closing fence with the real nonce. Add a byte-level (LC_ALL=C) normalization pass before the marker-phrase match: fold common Unicode space separators (NBSP, en/em/thin/hair spaces U+2000-200A, narrow NBSP U+202F, medium math space U+205F, ideographic space U+3000) to ASCII space, and strip zero-width/invisible characters (ZWSP/ZWNJ/ZWJ U+200B-200D, word joiner U+2060, BOM U+FEFF). The existing ASCII-whitespace-tolerant marker match then catches all these variants automatically. Add fixtures 6c/6d/6e forging the closing fence with the real nonce via NBSP, narrow NBSP, and a zero-width space split inside "UNTRUSTED" itself. Verified each fails against the pre-fix script and passes after the fix (glibc's C.UTF-8 [[:space:]] already covers most other Unicode Zs spaces, e.g. ideographic space, pre-fix — NBSP/narrow-NBSP are the genuine gap).
|
feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed) |
11 similar comments
|
feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed) |
|
feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed) |
|
feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed) |
|
feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed) |
|
feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed) |
|
feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed) |
|
feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed) |
|
feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed) |
|
feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed) |
|
feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed) |
|
feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed) |
Issue #94 — prompt-injection hardening (slice 1 of 4: untrusted-data fencing helper)
First, safely-scoped slice of #94. Delivers Layer 1 — the single shared sanitizer primitive the issue calls for ("one shared helper so there is exactly one implementation") — plus an adversarial test suite. Layers 2–4 are filed as follow-ups (see below).
What's in this PR
New files, both under
.claude/scripts/(moduleharness):sanitize-untrusted.sh— reads untrusted text (stdin or file arg), emits a nonce-fenced, sanitized rendering to stdout:[BEGIN/END UNTRUSTED USER CONTENT <nonce> …]markers with a per-invocation random nonce (SANITIZE_NONCEoverride for reproducible tests).LC_ALL=Cbyte-level pass, so the property does not rest on nonce secrecy alone.@mentions; neutralizefixes|closes|resolves #Nautolink keywords; escape HTML angle brackets; strip control/ANSI sequences; length cap (SANITIZE_MAX_CHARS, default 8000) with a truncation marker.sanitize-untrusted.test.sh— 28 offline checks, auto-discovered by thetestgate. Injection fixtures: "Additionally,"-style instruction (fenced as data), HTML injection, mention/fixes #Nabuse, control/ANSI, length cap, empty input, and fence-spoof with the real nonce across ASCII double-space/tab and Unicode NBSP/narrow-NBSP/zero-width-split variants.Gates
build,lint,testall green viaGATES_FILE=.claude/self/gates.json.Review
Scope / decision points (deferred to follow-ups, not decided here)
Layers 2–4 and the issue's two open decision points (self-hosted
protectedPathsoverride; whether an LLM threat lens is worth it for v1) are left to the follow-up issues. Homoglyph LETTER substitution / full NFKC-confusables of the marker words are out of scope for this deterministic v1 layer (backstopped by the secret nonce) and noted as a possible follow-up.Closes part of #94 (does not fully close it).
🤖 Generated with Claude Code