mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(redact): read content strictly before sanitizing or redacting it
Page and Form content was decoded tolerantly, keeping what came before a corrupt spot and dropping the rest without saying so. A page whose content broke at its first bytes therefore read as empty: the hidden-text scan counted nothing, the removal was skipped and the original stream, which other viewers still draw, stayed in the file. Content is now decoded strictly; an unreadable page counts as something to remove, so the removal runs and refuses the page, and a Form read only in part is treated like an undecodable one.
This commit is contained in:
@@ -1201,7 +1201,8 @@ fn xobject(
|
||||
out.doomed.push(r);
|
||||
return Some(None);
|
||||
}
|
||||
let data = match s.decoded() {
|
||||
// Strict, like page content: a Form read only in part counts as undecodable.
|
||||
let data = match s.decoded_strict() {
|
||||
Ok(d) => d,
|
||||
Err(_) if scope.mode == Mode::Apply => {
|
||||
scope.report.forms_removed += 1;
|
||||
|
||||
@@ -231,7 +231,9 @@ pub(crate) fn page_streams(doc: &Document, page: &Dict, index: usize) -> Result<
|
||||
let mut data = Vec::new();
|
||||
for o in &list {
|
||||
match &*doc.resolve(o) {
|
||||
Object::Stream(s) => data.push(s.decoded().map_err(|_| RedactError::Unreadable(index + 1))?),
|
||||
// Strict: a tolerant decode keeps what precedes corruption and drops the rest, which
|
||||
// still draws in viewers that read further; anything not read can't be cleaned.
|
||||
Object::Stream(s) => data.push(s.decoded_strict().map_err(|_| RedactError::Unreadable(index + 1))?),
|
||||
Object::Null => data.push(Vec::new()),
|
||||
_ => return Err(RedactError::Unreadable(index + 1)),
|
||||
}
|
||||
|
||||
@@ -205,6 +205,9 @@ fn content_pass(doc: &mut Document, text: bool, layers: bool, write: bool) -> Re
|
||||
if write {
|
||||
return Err(RedactError::Unreadable(pi + 1));
|
||||
}
|
||||
// Content that can't be read may hide anything: count it so the removal runs and
|
||||
// refuses the page, instead of a zero count skipping it.
|
||||
total += 1;
|
||||
continue;
|
||||
};
|
||||
let resources = page.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default();
|
||||
|
||||
@@ -2494,3 +2494,23 @@ fn a_name_tree_whose_nodes_share_kids_counts_each_entry_once() {
|
||||
let counts: std::collections::HashMap<Hidden, usize> = scan(&doc).into_iter().collect();
|
||||
assert_eq!(counts[&Hidden::Attachments], 1, "one entry, reached through 2^{levels} paths");
|
||||
}
|
||||
|
||||
/// Page content that stops decoding at its start (`z` inside an ASCII85 group) while the rest of
|
||||
/// the stream draws hidden text. A tolerant decode reads nothing, counts nothing and keeps the
|
||||
/// stream; sanitizing must refuse instead.
|
||||
#[test]
|
||||
fn hidden_text_in_content_that_stops_decoding_is_refused() {
|
||||
use crate::sanitize::{Hidden, remove_hidden, sanitize};
|
||||
let encoded: &[u8] = br"!!z6<#'\7PQ#?0Ha>,+>Y->EZd(j+>PW)+B2ko-rWQ%;FOP6+B3(u78s~>";
|
||||
let fresh = || {
|
||||
let mut doc = one_page(b"", "", vec![stream("/Filter /ASCII85Decode", encoded)]);
|
||||
doc.update_dict(ObjRef::new(3, 0), |d| d.set(b"Contents".to_vec(), Object::Ref(ObjRef::new(7, 0)))).unwrap();
|
||||
doc
|
||||
};
|
||||
let mut doc = fresh();
|
||||
let out = remove_hidden(&mut doc, &[Hidden::HiddenText]);
|
||||
assert!(matches!(out, Err(RedactError::Unreadable(1))), "{out:?}");
|
||||
let mut doc = fresh();
|
||||
let out = sanitize(&mut doc);
|
||||
assert!(matches!(out, Err(RedactError::Unreadable(1))), "{out:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user