Merge pull request #2244 from dotCooCoo/fix/pdf-nested-stream-checks

Importers: PDF streams written inside arrays and dictionaries are checked before they are decoded
This commit is contained in:
BFlat
2026-10-16 01:56:38 +07:00
committed by GitHub
2 changed files with 33 additions and 17 deletions
+20 -1
View File
@@ -2897,6 +2897,20 @@ fn pdf_streams_the_reader_would_fail_on_are_refused() {
format!("q 10 0 0 10 0 0 cm BI /W 2 /H 2 /CS /G /BPC 8 /F [/AHx /Fl] /DP [null {dp}] ID {data}> EI Q")
};
let form = |dict: &str, content: &str| pdf_with("/F Do", "<< /XObject << /F 5 0 R >> >>", &[packed(&format!("{FORM} {dict}"), content)]);
// A calculator function with the deflated program `z`, to write inside another object. Its
// data is in hexadecimal digits with spaces between them, so the object around it still reads.
let calculator = |z: &[u8], dp: &str| {
let data: String = z.iter().map(|b| format!("{:X} {:X} ", b >> 4, b & 15)).collect();
let dict = format!("/FunctionType 4 /Domain [0 1] /Range [0 1 0 1 0 1] /Filter [/ASCIIHexDecode /FlateDecode] {dp}");
format!("<< {dict} /Length {} >>\nstream\n{data}\nendstream", data.len())
};
let held = |object: String| pdf_with("0 0 5 5 re f", "<< >>", &[object.into_bytes()]);
let shaded = |function: &str| {
let shading = format!("<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Function {function} >>");
pdf_with("/S sh", "<< /Shading << /S 5 0 R >> >>", &[shading.into_bytes()])
};
let spaces = calculator(&spaces_past_the_cap(), "");
let program = deflated(b"{ dup 1 exch sub 0 }");
let files = [
("a form with Columns 0", form(zero, "0 0 5 5 re f")),
("a form with a huge Columns", form(huge, "0 0 5 5 re f")),
@@ -2905,6 +2919,10 @@ fn pdf_streams_the_reader_would_fail_on_are_refused() {
("an inline image in a compressed form", form("", &inline("<< /Predictor 2 /Columns 0 >>"))),
("an object stream with Columns 0", pdf_with_object_stream("[0 0 100 100]", zero)),
("a form of spaces past the cap", pdf_with("/F Do", "<< /XObject << /F 5 0 R >> >>", &[packed_as_is(FORM, &spaces_past_the_cap())])),
("a function of spaces past the cap in an array", held(format!("[{spaces}]"))),
("a function of spaces past the cap in a dictionary", held(format!("<< /K {spaces} >>"))),
("a function of spaces past the cap in a shading", shaded(&spaces)),
("a function with Columns 0 in an array", held(format!("[{}]", calculator(&program, "/DecodeParms [null << /Predictor 2 /Columns 0 >>]")))),
];
on_a_small_stack(move || {
for (what, bytes) in &files {
@@ -2916,7 +2934,8 @@ fn pdf_streams_the_reader_would_fail_on_are_refused() {
});
// Within range, the same files open.
let fine = "/DecodeParms << /Predictor 1 /Columns 0 >>";
for bytes in [form(fine, "0 0 5 5 re f"), deep_pdf(&inline("<< /Predictor 1 >>"), "<< >>")] {
let function = calculator(&program, "");
for bytes in [form(fine, "0 0 5 5 re f"), deep_pdf(&inline("<< /Predictor 1 >>"), "<< >>"), held(format!("[{function}]")), shaded(&function)] {
vectorcraft_engine::cmd::fileio::load("fine.pdf", &bytes).expect("opens");
}
}
+13 -16
View File
@@ -117,37 +117,34 @@ fn procedures_too_deep(o: &Object<'_>) -> bool {
/// The error of a file with a stream the reader would fail on ([`crate::streams`]).
const STREAM_NOTE: &str = "a stream in it would decode to more than 256 MB, or its decode parameters can't be applied";
/// The error of a file [`nests_without_end`].
/// The error of a file with functions or color spaces built on one another past [`MAX_LINKS`]
/// (or on themselves), or a calculator function nested past [`MAX_PROCEDURES`].
const NESTED_NOTE: &str = "its functions or colour spaces are built on one another without end";
/// Why `pdf` is refused, if it is: a stream the reader would fail on (its own checks come first:
/// the calculator functions are decoded below), or [`nests_without_end`].
/// Why `pdf` is refused, if it is: a stream the reader would fail on ([`STREAM_NOTE`]), or
/// functions, color spaces or calculator procedures nested too deep ([`NESTED_NOTE`]). Every
/// object is looked at, and every array, dictionary and stream written inside one, wherever it
/// is used from: the interpreter builds functions and color spaces from resources, shadings,
/// images, patterns, masks and graphics states alike.
fn hostile(pdf: &Pdf) -> Option<&'static str> {
if pdf.objects().into_iter().any(|o| matches!(&o, Object::Stream(s) if crate::streams::unsafe_stream(s))) {
return Some(STREAM_NOTE);
}
nests_without_end(pdf).then_some(NESTED_NOTE)
}
/// Does `pdf` hold functions or colour spaces built on one another past [`MAX_LINKS`] (or on
/// themselves), or a calculator function nested past [`MAX_PROCEDURES`]? Every object is looked
/// at, wherever it is used from: the interpreter builds them from resources, shadings, images,
/// patterns, masks and graphics states alike.
fn nests_without_end(pdf: &Pdf) -> bool {
let xref = pdf.xref();
let mut known = HashMap::new();
// The objects of the file, then what each holds directly (64 deep at most: the reader's cap).
let mut pending: Vec<Object<'_>> = pdf.objects().into_iter().collect();
while let Some(o) = pending.pop() {
// A stream's own checks come first: a calculator function is decoded below.
if matches!(&o, Object::Stream(s) if crate::streams::unsafe_stream(s)) {
return Some(STREAM_NOTE);
}
if procedures_too_deep(&o) || built_on(MaybeRef::NotRef(o.clone()), xref, &mut known, MAX_LINKS).is_none() {
return true;
return Some(NESTED_NOTE);
}
match &o {
Object::Array(a) => pending.extend(a.raw_iter().filter_map(direct)),
_ => pending.extend(dict_or_stream(&o).into_iter().flat_map(|(d, _)| values(d)).filter_map(direct)),
}
}
false
None
}
/// Read a PDF, decrypting it with `password` (none: the empty user password). The permissions