#3736: HTML pictures load only from inside the automation read root

Finding 54. Opening an HTML file checks the file through its links, but
its pictures load relative to the folder the given path names. Through a
link outside the read root to a page inside it, pictures beside the link
(outside the root) were embedded. Each picture's canonical path is now
checked with check_read before it is read; unconfined opens are unchanged.

Test: tests::html_pictures_load_only_from_inside_the_read_root.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Martin Ball
2026-10-23 20:19:49 +01:00
co-authored by Claude Opus 5.5
parent 8ce3eb58c7
commit 02fffea7c9
2 changed files with 27 additions and 1 deletions
+4 -1
View File
@@ -506,7 +506,7 @@ impl LocalImages {
left.map(|_| ())
}
/// The canonical path `src` names, if it's inside the folder.
/// The canonical path `src` names, if it's inside the folder and may be read.
fn resolve(&self, src: &str) -> Option<std::path::PathBuf> {
let src = src.split(['#', '?']).next().unwrap_or("");
if src.is_empty() || src.starts_with("//") || src.split_once(':').is_some_and(|(scheme, _)| !scheme.contains('/')) {
@@ -520,6 +520,9 @@ impl LocalImages {
let dir = self.dir.as_ref()?;
// Canonical paths have `..` and symlinks resolved, so this keeps both inside the folder.
let path = std::fs::canonicalize(dir.join(rel)).ok()?;
// The folder is the one the HTML path names, which may be a link from outside the
// automation read root; the picture itself must be inside it (#3736).
crate::file_access::check_read(&path.to_string_lossy()).ok()?;
path.starts_with(dir).then_some(path)
}
}
+23
View File
@@ -4112,6 +4112,29 @@ fn html_pictures_through_a_symlink_out_of_the_folder_are_not_loaded() {
assert!(doc.plain_text(StoryRef::Body).contains("Link"));
}
#[cfg(unix)]
#[test]
fn html_pictures_load_only_from_inside_the_read_root() {
// #3736: the HTML file is checked through its link, but its pictures load from beside the
// link. Opened through a link outside the read root, they must not come from there.
let root = scratch_dir("html-img-root");
for d in ["in", "outside"] {
std::fs::create_dir_all(root.join(d)).unwrap();
}
std::fs::write(root.join("in/page.html"), r#"<p><img src="pic.png" alt="Pic"></p>"#).unwrap();
std::fs::write(root.join("in/pic.png"), tiny_png()).unwrap();
std::fs::write(root.join("outside/pic.png"), tiny_png()).unwrap();
std::os::unix::fs::symlink(root.join("in/page.html"), root.join("outside/page.html")).unwrap();
let roots = crate::file_access::AutomationRoots::new(Some(&root.join("in")), None).unwrap();
let through_link = crate::file_access::confine(roots.as_ref(), || crate::io::open_path(&root.join("outside/page.html")));
let direct = crate::file_access::confine(roots.as_ref(), || crate::io::open_path(&root.join("in/page.html")));
let _ = std::fs::remove_dir_all(&root);
let through_link = through_link.unwrap();
assert!(through_link.media.is_empty(), "no picture from outside the read root");
assert!(through_link.plain_text(StoryRef::Body).contains("Pic"));
assert_eq!(direct.unwrap().media.len(), 1, "pictures inside the root still load");
}
/// `file.open` on a path reads only a regular file: a named pipe blocked until a writer came, and
/// `/dev/zero` read until memory ran out.
#[cfg(unix)]