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:
@@ -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
|
||||
|
||||
@@ -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('ab');
|
||||
const report = declaredRuntimeReport(`a${BIDI_RLO}b`);
|
||||
assert.strictEqual(report.mismatches[0].declaredRuntime, 'ab');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user