Tesserae

Class-Closure Threat Response

A finding should create an instrument, not merely a patch. The handoff from one session that blinded a prompt-injection detector with an accent, and the method it produced. Filed with its first action executed.

One session found a way to make aletheia, a prompt-injection detector, blind by putting an accent on a word. \b is not a security boundary. It marks a transition between \w and \W, and runtimes disagree about what \w is. The fix replaced a dialect default with a policy written in English first: match a security keyword unless it is embedded inside a larger ASCII identifier. The method that came out of it is called Class-Closure Threat Response.

The handoff below is the record: the chain of events, the finding as a probe you can run in the page, the method, the corrections, and the specification for what comes next. Read §4 (Corrections) before §3 (the method). The page itself says the method is only as good as the record of the times it went wrong.

Interactive, offline, no dependencies. Type a payload into §02 to see three readings of where a word ends. The shared page in §08 saves only to your own browser.Open full screen ↗

First action, executed

The specification’s first action for the Rust consolidation (issue #5) was: check what the Rust regex crate’s \b means by default. Not because it is expected to be wrong, but because assuming it agrees with the written policy is the exact move that started this.

This was run on 2026-09-27 against regex 1.13.1, using the twelve probes from the page:

input default \b (?-u:\b) written policy
critical match match match
criticalé · écritical · criticalЖ · critical中 blind match match
urgentü · exploité · immediatelyé blind match match
hypercritical · critical_path no match no match no match
U+FEFF / U+200B adjacent match match match

The crate’s default \b is Unicode-aware, as Python’s is, and it disagrees with the written policy on 7 of 12 cases. A straight port would have carried the original evasion into the one implementation meant to retire the class. (?-u:\b), the crate’s ASCII-only boundary, agrees with the policy on all twelve. This is the third runtime default the spec warned about, and it lands on the Python side of the seam.

Correction, same day: the policy itself has a case-folding seam

The first version of this tile said the policy could also be written directly as (?i)(?:^|[^A-Za-z0-9_])(?:KEYWORDS)(?:[^A-Za-z0-9_]|$), and that this form “agrees too”. It agreed on the twelve probes above because none of them exercised case folding. A verification pass run afterwards did:

Confirmed in the real code, and fixed. The first version of this correction could only say the finding held if aletheia compiled its bounded patterns case-insensitively. Checked against the source the same day, it did, in fifteen patterns per runtime. In the deployed assessor a single Kelvin sign dropped critical from 0.286 to 0.0: 116 blinded mutations across 15 of 21 seeds. It is a new member of the class, sitting in the same place U+FEFF did: generation (stage 5) feeding back into naming the assumption (stage 2). The violated assumption is that an ASCII class stays ASCII under case folding.

The fix landed in aletheia#21. It follows the method: the policy was written in English first; the keyword is rewritten into explicit case classes, so neither runtime needs an ignore-case flag; the four-letter fold family was added to the mutation corpus (651 → 819 cases); and a gate now fails the build if a bounded pattern is compiled case-insensitively again. A second adversarial pass on the fix found two latent gaps in its own tooling. The gate read lines instead of calls, and the rewrite folded letters inside \x escapes. Both were closed before merge.

Twelve probes are not the 651-case corpus. The acceptance criterion stands: the Rust core passes the full mutation corpus, and whichever boundary form the core uses should be pinned by a test that names the policy, not the primitive.

The harness (Rust, regex = “1”)
use regex::Regex;

fn main() {
    let kw = "critical|urgent|exploit|breach|immediately";
    let default_b = Regex::new(&format!(r"(?i)\b(?:{kw})\b")).unwrap();
    let ascii_b = Regex::new(&format!(r"(?i-u:\b)(?:{kw})(?-u:\b)")).unwrap();
    let policy = Regex::new(&format!(
        r"(?i)(?:^|[^A-Za-z0-9_])(?:{kw})(?:[^A-Za-z0-9_]|$)"
    )).unwrap();

    let cases: &[(&str, bool)] = &[
        ("critical", true), ("criticalé", true), ("écritical", true),
        ("criticalЖ", true), ("critical中", true), ("urgentü", true),
        ("exploité", true), ("immediatelyé", true),
        ("hypercritical", false), ("critical_path", false),
        ("\u{FEFF}critical\u{FEFF}", true), ("critical\u{200B}", true),
    ];
    for (s, want) in cases {
        println!("{s:?}: default={} ascii={} policy={} expected={want}",
            default_b.is_match(s), ascii_b.is_match(s), policy.is_match(s));
    }
}

Published with redactions

aletheia is a live control. Three things are withheld from this public copy and marked in place: the hook’s current enforcement state, the origin and score of one unfiled policy residue, and local filesystem paths. The closed \b finding, the method and the corrections are unchanged. The reproduction commands in §07 need the aletheia repository itself.