From e0519d4300abe8394d6d4f9c1e67a7789863a0f0 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 24 Oct 2026 06:02:56 +0000 Subject: [PATCH] Reject signed numeric character references in XML entity decoding Found while testing crates/meta/src/xmp.rs:184 (entity); the same pattern is in crates/lensdb/src/xml.rs:45 and crates/engine/src/preset_luminar.rs:90 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/preset_luminar.rs | 9 ++++++++- crates/lensdb/src/xml.rs | 19 +++++++++++++++++-- crates/meta/src/xmp.rs | 14 ++++++++++++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/preset_luminar.rs b/crates/engine/src/preset_luminar.rs index e883045684..73b008d9ff 100644 --- a/crates/engine/src/preset_luminar.rs +++ b/crates/engine/src/preset_luminar.rs @@ -89,8 +89,9 @@ fn unescape(t: &str) -> String { _ => ent .strip_prefix("#x") .or_else(|| ent.strip_prefix("#X")) + .filter(|h| h.bytes().all(|b| b.is_ascii_hexdigit())) .and_then(|h| u32::from_str_radix(h, 16).ok()) - .or_else(|| ent.strip_prefix('#').and_then(|d| d.parse().ok())) + .or_else(|| ent.strip_prefix('#').filter(|d| d.bytes().all(|b| b.is_ascii_digit())).and_then(|d| d.parse().ok())) .and_then(char::from_u32), }; match c { @@ -485,6 +486,12 @@ mod tests { use crate::preset_import::read_presets; use serde_json::json; + #[test] + fn unescape_rejects_signed_numeric_references() { + assert_eq!(unescape("&#+65;&#x+41;"), "&#+65;&#x+41;"); + assert_eq!(unescape("AA"), "AA"); + } + /// One effect's parameter as Luminar writes it. fn param(name: &str, v: f64) -> String { format!("{name}OptionalDataType0Value{v}") diff --git a/crates/lensdb/src/xml.rs b/crates/lensdb/src/xml.rs index dc611a647c..5d4a39dc8c 100644 --- a/crates/lensdb/src/xml.rs +++ b/crates/lensdb/src/xml.rs @@ -43,8 +43,10 @@ fn entity(name: &str) -> Option { _ => { let n = name.strip_prefix('#')?; let v = match n.strip_prefix('x') { - Some(h) => u32::from_str_radix(h, 16).ok()?, - None => n.parse().ok()?, + Some(h) if h.bytes().all(|b| b.is_ascii_hexdigit()) => u32::from_str_radix(h, 16).ok()?, + Some(_) => return None, + None if n.bytes().all(|b| b.is_ascii_digit()) => n.parse().ok()?, + None => return None, }; char::from_u32(v)? } @@ -134,3 +136,16 @@ fn close(stack: &mut Vec) { p.children.push(n); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn entity_rejects_signed_numeric_references() { + assert_eq!(entity("#+65"), None); + assert_eq!(entity("#x+41"), None); + assert_eq!(entity("#65"), Some('A')); + assert_eq!(entity("#x41"), Some('A')); + } +} diff --git a/crates/meta/src/xmp.rs b/crates/meta/src/xmp.rs index 2dbf59482e..6f8a7d4023 100644 --- a/crates/meta/src/xmp.rs +++ b/crates/meta/src/xmp.rs @@ -203,8 +203,10 @@ fn entity(name: &str) -> Option { _ => { let n = name.strip_prefix('#')?; let v = match n.strip_prefix(['x', 'X']) { - Some(h) => u32::from_str_radix(h, 16).ok()?, - None => n.parse().ok()?, + Some(h) if h.bytes().all(|b| b.is_ascii_hexdigit()) => u32::from_str_radix(h, 16).ok()?, + Some(_) => return None, + None if n.bytes().all(|b| b.is_ascii_digit()) => n.parse().ok()?, + None => return None, }; char::from_u32(v).filter(|&c| is_xml_char(c))? } @@ -1171,4 +1173,12 @@ mod tests { assert_eq!(entity("#x41"), Some('A')); assert_eq!(entity("#9"), Some('\t')); } + + #[test] + fn entity_rejects_signed_numeric_references() { + assert_eq!(entity("#+65"), None); + assert_eq!(entity("#x+41"), None); + assert_eq!(entity("#65"), Some('A')); + assert_eq!(entity("#x41"), Some('A')); + } }