Skip to main content

rtl_flip_detect/
lib.rs

1//! # rtl-flip-detect
2//!
3//! Detect bidi-control characters that flip rendered direction.
4//!
5//! The classic attack: a filename `evil\u{202E}cod.exe` renders as
6//! `evilexe.doc` because U+202E (RIGHT-TO-LEFT OVERRIDE) flips
7//! everything after it. Same trick works inside any text the model
8//! displays back, or a tool argument.
9//!
10//! This crate finds and strips those.
11//!
12//! Controls flagged:
13//! - U+202A LRE, U+202B RLE, U+202D LRO, U+202E RLO
14//! - U+202C PDF (pop directional formatting — could close an attacker's open)
15//! - U+2066 LRI, U+2067 RLI, U+2068 FSI, U+2069 PDI
16//!
17//! ## Example
18//!
19//! ```
20//! use rtl_flip_detect::{has_rtl_flip, strip_rtl_flips};
21//! let evil = "evil\u{202E}cod.exe";
22//! assert!(has_rtl_flip(evil));
23//! assert_eq!(strip_rtl_flips(evil), "evilcod.exe");
24//! ```
25
26#![deny(missing_docs)]
27
28/// True when the input contains any bidi-control char that could flip
29/// direction.
30pub fn has_rtl_flip(s: &str) -> bool {
31    s.chars().any(is_bidi_control)
32}
33
34/// Return the byte positions of every bidi-control char in `s`.
35pub fn find_rtl_flips(s: &str) -> Vec<(usize, char)> {
36    s.char_indices()
37        .filter(|(_, c)| is_bidi_control(*c))
38        .collect()
39}
40
41/// Strip every bidi-control char from `s`.
42pub fn strip_rtl_flips(s: &str) -> String {
43    s.chars().filter(|c| !is_bidi_control(*c)).collect()
44}
45
46fn is_bidi_control(c: char) -> bool {
47    matches!(c as u32,
48        0x202A..=0x202E
49        | 0x2066..=0x2069
50    )
51}