nickserv/chanserv: catch styled/fullwidth look-alike names too, and add a [register] confusable_check toggle (REHASH-reloadable)
All checks were successful
CI / check (push) Successful in 3m48s

This commit is contained in:
Jean Chevronnet 2026-07-19 00:15:08 +00:00
parent c2b70f5004
commit 89ac01d0b0
No known key found for this signature in database
11 changed files with 72 additions and 12 deletions

View file

@ -1935,6 +1935,11 @@ pub trait Store {
// Whether account identity is owned externally (website); when true, IRC
// can't register or change credentials — it only authenticates.
fn external_accounts(&self) -> bool;
// Whether REGISTER should reject look-alike / mixed-script names ([register]
// confusable_check). Defaults on; a mock store need not override it.
fn confusable_check_enabled(&self) -> bool {
true
}
// Staff notes on accounts/channels (oper-only), shown in INFO to operators.
fn set_account_note(&mut self, account: &str, note: Option<String>) -> bool;
fn account_note(&self, account: &str) -> Option<String>;
@ -2175,6 +2180,12 @@ pub fn confusable_reason(name: &str) -> Option<&'static str> {
fn is_invisible(cp: u32) -> bool {
matches!(cp, 0xAD | 0x180E | 0x200B..=0x200F | 0x202A..=0x202E | 0x2060 | 0x2066..=0x2069 | 0xFEFF)
}
// "Styled" Latin that renders as normal-looking ASCII: fullwidth (),
// mathematical alphanumerics (𝐚𝐝𝐦𝐢𝐧), and enclosed/circled/squared letters.
// Nobody types a whole name this way except to imitate one.
fn is_fancy_latin(cp: u32) -> bool {
matches!(cp, 0xFF21..=0xFF5A | 0x1D400..=0x1D7FF | 0x1F130..=0x1F189 | 0x24B6..=0x24E9 | 0x2460..=0x24FF)
}
// Non-Latin letters that look like an ASCII Latin letter — the homoglyphs a
// spoofer swaps in. Catches an all-look-alike name that pure script-mixing
// would miss (e.g. an entirely-Cyrillic name that reads as Latin).
@ -2198,6 +2209,9 @@ pub fn confusable_reason(name: &str) -> Option<&'static str> {
if is_invisible(cp) {
return Some("That name uses invisible or text-direction characters. Please choose a plain name.");
}
if is_fancy_latin(cp) {
return Some("That name uses styled look-alike letters (fullwidth or symbol fonts). Please choose a plain name.");
}
if let Some((script, id)) = script_id(cp) {
letters += 1;
if script == Script::Latin {
@ -2349,6 +2363,8 @@ mod tests {
"аеосх", // all-Cyrillic Latin look-alikes — homoglyph
"ad\u{200b}min", // zero-width space
"\u{202e}nimda", // right-to-left override
"", // fullwidth Latin
"\u{1D41A}\u{1D41D}\u{1D426}\u{1D422}\u{1D427}", // 𝐚𝐝𝐦𝐢𝐧 mathematical bold
] {
assert!(confusable_reason(bad).is_some(), "should be rejected: {bad}");
}