fix(#2873): write bidi codepoints as escapes, not literals

The invisible-Unicode scan flagged the compiled sanitizer: BIDI_RE in
install-shadow-report.cts carried literal U+202A-U+202E where its two
neighbouring regexes already used \u{...} escapes, so the module that
strips bidi controls was itself a carrier for them.

The prompt-injection-scan failure alongside it was the same defect rolling
up through the parent describe, not a second cause - verified by running
the scanner across every category it checks.

Test fixtures and property generators now name their codepoints (RLO, LRE,
PDI) instead of embedding invisible bytes, so a reviewer can see which
character is under test.

Refs #2873
This commit is contained in:
sim
2026-08-15 00:02:37 -04:00
parent 2d72577e07
commit d37e594ec8
3 changed files with 23 additions and 5 deletions

View File

@@ -173,7 +173,7 @@ const CONTROL_RE = /[\x00-\x1f\x7f-\x9f]/g;
/** Unicode bidi embedding/override controls (U+202A-U+202E) and bidi
* isolates (U+2066-U+2069) — the RTL-spoofing class the design doc's row
* #13 names. */
const BIDI_RE = /[‪-‮⁦-⁩]/g;
const BIDI_RE = /[\u{202A}-\u{202E}\u{2066}-\u{2069}]/gu;
/** Combining marks (U+0300-U+036F) — "zalgo" text. Stacked onto the
* preceding base character, an unbounded run visually overflows into

View File

@@ -80,6 +80,12 @@ function declaredRuntimeReport(runtimeVal) {
return buildShadowReport('claude', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) }));
}
// RLO (Right-to-Left Override, U+202E) — written as a `\u{...}` escape (not
// a literal bidi character) so the source stays plain ASCII and does not
// carry the very invisible/dangerous-Unicode class it tests. See the
// matching B17/B18 note further below for the same rationale.
const BIDI_RLO = '\u{202E}';
// ─── B1-B4 — hostile declaredRuntime payloads are neutralized in the IR ────
describe('buildShadowReport — hostile declaredRuntime is sanitized in the IR (B1-B4)', () => {
@@ -109,7 +115,7 @@ describe('buildShadowReport — hostile declaredRuntime is sanitized in the IR (
});
test('bidi override is stripped (B4)', () => {
const report = declaredRuntimeReport('a‮b');
const report = declaredRuntimeReport(`a${BIDI_RLO}b`);
assert.strictEqual(report.mismatches[0].declaredRuntime, 'ab');
});
});

View File

@@ -589,9 +589,21 @@ describe('sanitizeForRender — properties (F2, F3)', () => {
const delC1Arb = fc.integer({ min: 0x7f, max: 0x9f }).map((c) => String.fromCharCode(c));
const ansiCsiArb = fc.constantFrom('\x1b[31m', '\x1b[0m', '\x1b[2K', '\x1b[1;37;40m');
const ansiOscArb = fc.constantFrom('\x1b]0;title\x07', '\x1b]8;;http://example\x1b\\');
// Bidi embedding/override controls (U+202A-U+202E) and isolates
// (U+2066-U+2069), written as `\u{...}` escapes rather than literal
// characters — see the #2873 PR review Finding 2 note above.
const BIDI_LRE = '\u{202A}'; // Left-to-Right Embedding
const BIDI_RLE = '\u{202B}'; // Right-to-Left Embedding
const BIDI_PDF = '\u{202C}'; // Pop Directional Formatting
const BIDI_LRO = '\u{202D}'; // Left-to-Right Override
const BIDI_RLO = '\u{202E}'; // Right-to-Left Override
const BIDI_LRI = '\u{2066}'; // Left-to-Right Isolate
const BIDI_RLI = '\u{2067}'; // Right-to-Left Isolate
const BIDI_FSI = '\u{2068}'; // First Strong Isolate
const BIDI_PDI = '\u{2069}'; // Pop Directional Isolate
const bidiArb = fc.constantFrom(
'‪', '‫', '‬', '‭', '‮', // embedding/override
'⁦', '⁧', '⁨', '⁩', // isolates
BIDI_LRE, BIDI_RLE, BIDI_PDF, BIDI_LRO, BIDI_RLO, // embedding/override
BIDI_LRI, BIDI_RLI, BIDI_FSI, BIDI_PDI, // isolates
);
const combiningArb = fc.constantFrom('\u{0300}', '\u{0301}', '\u{0302}', '\u{036F}');
const zeroWidthArb = fc.constantFrom('\u{200B}', '\u{200C}', '\u{200D}', '\u{FEFF}');
@@ -612,7 +624,7 @@ describe('sanitizeForRender — properties (F2, F3)', () => {
// against the module's documented contract, not a tautology against its
// own internals.
// eslint-disable-next-line no-control-regex, no-misleading-character-class
const STRIPPED_CLASS_RE = /[\x00-\x1f\x7f-\x9f‪-‮⁦-⁩\u{0300}-\u{036F}\u{200B}-\u{200D}\u{FEFF}]/u;
const STRIPPED_CLASS_RE = /[\x00-\x1f\x7f-\x9f\u{202A}-\u{202E}\u{2066}-\u{2069}\u{0300}-\u{036F}\u{200B}-\u{200D}\u{FEFF}]/u;
test('sanitizer is idempotent: s(s(x)) === s(x) for arbitrary strings (F2)', () => {
let changedCount = 0;