security(codecs): stop quadratic rescans in XMP value scanning

xmp_values advanced its cursor only past the matched key, so the
value-closing search (find(close)) rescanned the remaining packet for
every property-like substring. Repeating text such as
' tiff:ResolutionUnit>' inside the packet produced quadratic CPU work
even when no real resolution property existed.

Advance the cursor past each located value; a missing close delimiter
means the value is unterminated, so no later value can parse and the
scan stops. Valid multi-value extraction is unchanged.

Finding: 6ce55e33-27f0-4fb9-a22f-737382e1d328 (CWE-407)
This commit is contained in:
Dylan M. Taylor
2026-10-22 07:11:45 -04:00
parent ac7eca536a
commit 19e8c0d7cd
2 changed files with 18 additions and 1 deletions
+7 -1
View File
@@ -429,8 +429,14 @@ fn xmp_values(xmp: &str, key: &str) -> Vec<(usize, usize)> {
continue; continue;
}; };
let start = xmp.len() - value.len(); let start = xmp.len() - value.len();
let Some(len) = value.find(close) else { continue }; // Advance past this value before searching for the next key. Without this, `from`
// only moved past the key text, so property-like text repeated inside an unrelated
// attribute re-ran this full-suffix `find(close)` for every occurrence, which is
// quadratic in the packet size. A missing close means the value is unterminated, so
// no later value can parse either; stop scanning in that case.
let Some(len) = value.find(close) else { break };
out.push((start, start + len)); out.push((start, start + len));
from = start + len + close.len_utf8();
} }
out out
} }
+11
View File
@@ -284,6 +284,17 @@ fn truncated_metadata_never_panics_and_falls_back() {
assert_eq!(xmp_resolution(x), None, "{x}"); assert_eq!(xmp_resolution(x), None, "{x}");
let _ = xmp_with_resolution(x, (1.0, 1.0)); let _ = xmp_with_resolution(x, (1.0, 1.0));
} }
// Property-like text repeated inside the packet must not trigger a full-suffix rescan
// per occurrence (it used to be quadratic). With the cursor advancing past each located
// value, this completes well under a second even with many decoys; a timeout here would
// indicate the quadratic scan returned. A clean packet still extracts correctly.
let start = std::time::Instant::now();
let decoys = " tiff:ResolutionUnit>".repeat(50_000);
let _ = xmp_resolution(&decoys);
assert!(start.elapsed() < std::time::Duration::from_secs(2), "xmp scan took too long: {:?}", start.elapsed());
let clean = "<tiff:XResolution>300/1</tiff:XResolution><tiff:YResolution>300/1</tiff:YResolution><tiff:ResolutionUnit>2</tiff:ResolutionUnit>";
near(xmp_resolution(clean), 300.0);
} }
proptest! { proptest! {