diff --git a/crates/tinyredact/src/pii/checks.rs b/crates/tinyredact/src/pii/checks.rs index e781295a..2ca04f83 100644 --- a/crates/tinyredact/src/pii/checks.rs +++ b/crates/tinyredact/src/pii/checks.rs @@ -170,8 +170,17 @@ pub(crate) fn plausible_card_number(d: &[u32]) -> bool { // IBAN mod-97. Steps: strip spaces, move first 4 chars to end, expand letters // (A=10..Z=35), divide as a big-integer mod 97, require remainder == 1. +// +// `IBAN_RE` matches with Unicode `\d`, so a candidate can carry digits from +// another script (Devanagari `४`, Bengali `৪`, ...) that normalization does +// not fold. An IBAN is ASCII by definition, so anything else is rejected +// before the byte-offset slicing below, which would otherwise split a +// multi-byte digit and panic. pub(crate) fn valid_iban(s: &str) -> bool { let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect(); + if !cleaned.bytes().all(|b| b.is_ascii_alphanumeric()) { + return false; + } if cleaned.len() < 15 || cleaned.len() > 34 { return false; } diff --git a/crates/tinyredact/src/pii/checks_tests.rs b/crates/tinyredact/src/pii/checks_tests.rs index ebcafadf..d396ce8f 100644 --- a/crates/tinyredact/src/pii/checks_tests.rs +++ b/crates/tinyredact/src/pii/checks_tests.rs @@ -25,6 +25,14 @@ fn payment_checksums_reject_bad_bounds_and_checksums() { assert!(!valid_iban("GB00")); } +#[test] +fn iban_rejects_non_ascii_digits_without_panicking() { + // `IBAN_RE`'s `\d` is Unicode, so a Devanagari digit can reach the + // validator; the check digits used to be sliced by byte and split it. + assert!(!valid_iban("DE\u{096A}\u{096A}1234567890123456")); + assert!(!valid_iban("DE89\u{096A}70400440532013000")); +} + #[test] fn identity_validators_cover_checksums_reserved_values_and_prefixes() { assert!(valid_verhoeff(&digits("234567890124"))); diff --git a/crates/tinyredact/src/pii/mod_tests.rs b/crates/tinyredact/src/pii/mod_tests.rs index 7cd023df..22616e4c 100644 --- a/crates/tinyredact/src/pii/mod_tests.rs +++ b/crates/tinyredact/src/pii/mod_tests.rs @@ -259,6 +259,15 @@ fn iban_de_redacted() { fn iban_invalid_kept() { unchanged("noise DE89370400440532013001 noise"); } +#[test] +fn iban_with_non_ascii_digits_does_not_panic() { + // Devanagari check digits match `IBAN_RE`'s Unicode `\d` but are not + // folded by normalization; both the content and boundary paths used to + // panic on them instead of leaving the text alone. + let text = "IBAN DE\u{096A}\u{096A}1234567890123456 end"; + unchanged(text); + assert!(!has_likely_pii(text)); +} // --- Aadhaar --- #[test]