diff --git a/crates/formats/src/html.rs b/crates/formats/src/html.rs index 4f2e4384f..3ba14405f 100644 --- a/crates/formats/src/html.rs +++ b/crates/formats/src/html.rs @@ -28,7 +28,14 @@ const MAX_STACK: usize = 256; /// Decode an entity body (`amp`, `#38`, `#x26`) to its text. pub fn decode_entity(name: &str) -> Option { if let Some(num) = name.strip_prefix('#') { - let v = if let Some(h) = num.strip_prefix(['x', 'X']) { u32::from_str_radix(h, 16).ok()? } else { num.parse::().ok()? }; + let (digits, radix) = match num.strip_prefix(['x', 'X']) { + Some(h) => (h, 16), + None => (num, 10), + }; + if !digits.chars().all(|c| c.is_digit(radix)) { + return None; + } + let v = u32::from_str_radix(digits, radix).ok()?; let c = match v { 0 => '\u{FFFD}', 0x80..=0x9F => crate::txt::cp1252(v as u8), @@ -1782,6 +1789,13 @@ pub fn export_flow(flow: &Flow, lang: &str) -> String { mod tests { use super::*; + #[test] + fn numeric_entity_rejects_sign() { + assert_eq!(unescape("a&#+65;b"), "a&#+65;b"); + assert_eq!(unescape("a&#x+41;b"), "a&#x+41;b"); + assert_eq!(unescape("aAAb"), "aAAb"); + } + fn texts(f: &Flow) -> Vec { f.blocks .iter()