Skip to content

Commit adcc086

Browse files
committed
fix: strip C1 control chars in JSON output alongside BiDi
JSON.stringify only escapes C0 (U+0000-U+001F) per RFC 8259; C1 controls (U+0080-U+009F, e.g. CSI=U+009B) pass through unescaped. stripBidi() now removes both C1 and BiDi chars, preventing terminal injection when --format json output is displayed in a terminal.
1 parent a1734e2 commit adcc086

1 file changed

Lines changed: 20 additions & 9 deletions

File tree

src/lib/formatters/local.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,26 @@ import {
88
mergeTransactionAttributes,
99
} from "./semantic-display.js";
1010

11-
/** Unicode bidirectional override/isolate characters that can reorder terminal output. */
11+
/**
12+
* Characters unsafe for JSON terminal display: C1 control characters
13+
* (U+0080–U+009F, e.g. CSI=U+009B) and Unicode bidirectional overrides.
14+
* `JSON.stringify` only escapes C0 (U+0000–U+001F) per RFC 8259;
15+
* C1 and BiDi pass through unescaped.
16+
*/
17+
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping C1 control chars from untrusted data
18+
const JSON_UNSAFE_RE = /[\x80-\x9f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
19+
20+
/** BiDi-only regex for the full `sanitize()` function. */
1221
const BIDI_RE = /[\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
1322

1423
/**
15-
* Strip Unicode bidirectional override characters from a string.
16-
* Used for JSON output where `JSON.stringify` handles C0/C1 escaping
17-
* but BiDi characters pass through and can reorder terminal text.
24+
* Strip C1 control characters and Unicode BiDi overrides from a string.
25+
* Used for JSON output where `JSON.stringify` escapes C0 controls but
26+
* leaves C1 (U+0080–U+009F) and BiDi chars intact — both can cause
27+
* terminal injection when JSON output is displayed in a terminal.
1828
*/
1929
export function stripBidi(text: string): string {
20-
return text.replace(BIDI_RE, "");
30+
return text.replace(JSON_UNSAFE_RE, "");
2131
}
2232

2333
/**
@@ -466,10 +476,11 @@ function formatLogJson(
466476
* coding agents and automation tools.
467477
*
468478
* Unlike the human formatters, JSON output uses `stripBidi()` instead of
469-
* the full `sanitize()`. `JSON.stringify()` escapes C0/C1 control characters
470-
* to `\uXXXX` notation, but Unicode bidirectional override characters pass
471-
* through and can reorder terminal text. `stripBidi()` removes those while
472-
* preserving the original data for downstream consumers.
479+
* the full `sanitize()`. `JSON.stringify()` escapes C0 control characters
480+
* (U+0000–U+001F) but leaves C1 controls (U+0080–U+009F) and BiDi overrides
481+
* intact. `stripBidi()` strips both, preventing terminal injection when
482+
* JSON output is viewed in a terminal, while preserving the original data
483+
* structure for downstream consumers.
473484
*/
474485
export function formatItemJson(
475486
itemType: string | undefined,

0 commit comments

Comments
 (0)