From c2cc79daa747bbbfcdec22bf1eea95a1d1ef8aa2 Mon Sep 17 00:00:00 2001 From: lenamonj Date: Wed, 29 Jul 2026 15:30:07 -0400 Subject: [PATCH 1/2] Fix --sanitize passing through the bidi control characters U+200E, U+200F and U+061C --- src/preprocessor.rs | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/preprocessor.rs b/src/preprocessor.rs index 971ae3d4..a9b8e712 100644 --- a/src/preprocessor.rs +++ b/src/preprocessor.rs @@ -168,7 +168,7 @@ pub fn sanitize(line: &str) -> String { #[inline] fn is_sanitize_trigger(b: u8) -> bool { // C0 controls minus \t \n \f; DEL; UTF-8 leads with dangerous codepoints. - matches!(b, 0x00..=0x08 | 0x0B | 0x0D..=0x1F | 0x7F | 0xC2 | 0xE2 | 0xEF) + matches!(b, 0x00..=0x08 | 0x0B | 0x0D..=0x1F | 0x7F | 0xC2 | 0xD8 | 0xE2 | 0xEF) } /// Substitutes the byte/sequence at `bytes[i]` (or passes it through on @@ -200,8 +200,14 @@ fn sanitize_at( buffer.push('\u{FFFD}'); 3 } + // 0xD8 0x9C = U+061C (Arabic letter mark, a bidi control). The rest of + // the 0xD8 block is ordinary Arabic text. + 0xD8 if bytes.get(i + 1) == Some(&0x9C) => { + buffer.push('\u{FFFD}'); + 2 + } // False-alarm trigger: pass the full UTF-8 sequence through. - lead @ (0xC2 | 0xE2 | 0xEF) => { + lead @ (0xC2 | 0xD8 | 0xE2 | 0xEF) => { let n = utf8_len_from_lead(lead); buffer.push_str(&full[i..i + n]); n @@ -217,10 +223,11 @@ fn sanitize_at( #[inline] fn is_dangerous_e2(bytes: &[u8], i: usize) -> bool { - // U+200B..D (zero-width), U+202A..E (bidi controls), U+2066..9 (isolates). + // U+200B..D (zero-width), U+200E..F (LRM/RLM), U+202A..E (bidi embedding + // and override), U+2066..9 (bidi isolates). matches!( (bytes.get(i + 1), bytes.get(i + 2)), - (Some(0x80), Some(0x8B..=0x8D | 0xAA..=0xAE)) | (Some(0x81), Some(0xA6..=0xA9)) + (Some(0x80), Some(0x8B..=0x8F | 0xAA..=0xAE)) | (Some(0x81), Some(0xA6..=0xA9)) ) } @@ -411,6 +418,34 @@ fn test_sanitize_substitutes_bidi_and_zero_width() { assert_eq!(sanitize("a\u{FEFF}b"), format!("a{r}b")); } +#[test] +fn test_sanitize_substitutes_every_bidi_control() { + // Unicode Bidi_Control is exactly these 12 codepoints. Covering only some + // of them leaves a reordering attack available through the rest. + let r = '\u{FFFD}'; + for c in [ + '\u{061C}', '\u{200E}', '\u{200F}', '\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', + '\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{2069}', + ] { + assert_eq!( + sanitize(&format!("a{c}b")), + format!("a{r}b"), + "U+{:04X} was not substituted", + c as u32 + ); + } +} + +#[test] +fn test_sanitize_preserves_arabic_sharing_the_alm_lead_byte() { + // U+061C is reached via lead byte 0xD8, which also leads ordinary Arabic. + assert_eq!( + sanitize("\u{0600}\u{061F}\u{06FF}"), + "\u{0600}\u{061F}\u{06FF}" + ); + assert_eq!(sanitize("مرحبا بالعالم"), "مرحبا بالعالم"); +} + #[test] fn test_sanitize_preserves_legitimate_bytes() { assert_eq!(sanitize("crlf\r\nline\r\n"), "crlf\r\nline\r\n"); From df382256532b96da7cc8eba8fb5dca2d67c5d8ce Mon Sep 17 00:00:00 2001 From: lenamonj Date: Wed, 29 Jul 2026 15:38:59 -0400 Subject: [PATCH 2/2] Add CHANGELOG entry for #3862 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cf4f8f7..7e0abc18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ ## Bugfixes - Fix `--list-languages` respecting `--paging=never`, see #3828 (@cyphercodes) +- Fix `--sanitize` passing through the bidi control characters U+200E, U+200F and U+061C, see #3862 (@lenamonj) - `--strip-ansi`: also strip 8-bit C1 introducers (U+0090, U+0098, U+009B, U+009D, U+009E, U+009F) and DCS/SOS/PM/APC sequence bodies, which previously passed through. See #3729 (@curious-rabbit) - Fix `--ignored-suffix` not falling back to first-line/shebang detection when the ignored suffix is also a registered extension (e.g. `--ignored-suffix .txt` on a shebang script), see #2745 and #3816 (@adnrivera) - Fix `capacity overflow` panic when printing a snip separator at `--terminal-width=1` with multiple line ranges. Closes #3803, see #3804 (@leeewee)