Skip to content

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1)#162

Merged
robercano-ghbot merged 3 commits into
mainfrom
feat/issue-94-sanitize-untrusted
Jul 20, 2026
Merged

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1)#162
robercano-ghbot merged 3 commits into
mainfrom
feat/issue-94-sanitize-untrusted

Conversation

@robercano-ghbot

Copy link
Copy Markdown
Collaborator

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/ (module harness):

  • sanitize-untrusted.sh — reads untrusted text (stdin or file arg), emits a nonce-fenced, sanitized rendering to stdout:
    • Nonce fence: wraps the body in [BEGIN/END UNTRUSTED USER CONTENT <nonce> …] markers with a per-invocation random nonce (SANITIZE_NONCE override for reproducible tests).
    • Anti-spoof (load-bearing): neutralizes any occurrence of the marker phrase in the body so untrusted input cannot forge a closing fence and smuggle post-fence instructions. Hardened across the whitespace/invisible-char class — ASCII whitespace runs, Unicode Zs spaces (NBSP, en/em/narrow/ideographic, …) folded to ASCII, and zero-width/BOM chars stripped — via a locale-independent LC_ALL=C byte-level pass, so the property does not rest on nonce secrecy alone.
    • Mechanical sanitization: defang @mentions; neutralize fixes|closes|resolves #N autolink keywords; escape HTML angle brackets; strip control/ANSI sequences; length cap (SANITIZE_MAX_CHARS, default 8000) with a truncation marker.
    • Pure bash + coreutils, offline. Not yet wired into the live loop — it is the primitive; wiring is a documented follow-up so nobody assumes protection is active.
  • sanitize-untrusted.test.sh — 28 offline checks, auto-discovered by the test gate. Injection fixtures: "Additionally,"-style instruction (fenced as data), HTML injection, mention/fixes #N abuse, 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, test all green via GATES_FILE=.claude/self/gates.json.

Review

  • correctness (opus): APPROVE — anti-spoof verified empirically, ordering/length-cap/exit paths sound.
  • tests (sonnet): APPROVE — after two hardening rounds closing the ASCII-whitespace then Unicode-whitespace/zero-width forged-fence classes; new fixtures proven fail-before/pass-after.

Scope / decision points (deferred to follow-ups, not decided here)

Layers 2–4 and the issue's two open decision points (self-hosted protectedPaths override; 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

robercano and others added 3 commits July 20, 2026 21:17
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).
@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed)

11 similar comments
@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed)

@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed)

@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed)

@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed)

@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed)

@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed)

@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed)

@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed)

@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed)

@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed)

@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

feat(harness): untrusted-text sanitizer primitive — nonce-fenced anti-spoof + injection tests (issue #94, slice 1) (not yet reviewed)

@robercano-ghbot
robercano-ghbot merged commit 94a7547 into main Jul 20, 2026
9 checks passed
@robercano-ghbot
robercano-ghbot deleted the feat/issue-94-sanitize-untrusted branch July 20, 2026 20:59
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.

2 participants