diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 08bdaf0ce3..6494aa55d4 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -384,6 +384,9 @@ fn export_png(s: &mut Session, v: &Value) -> CmdResult { // the on-screen cue while editing the body, not part of the page. let mut opts = wordcraft_render::RenderOptions::default(); opts.display.dim_header = false; + if !path.to_ascii_lowercase().ends_with(".png") { + return Err(CmdError::Params("path must end in .png".into())); + } let img = wordcraft_render::render_page(&s.doc, pg, scale, &opts); let png = img.to_png(); crate::io::write_file(path, &png).map_err(CmdError::Failed)?; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 8c777dba6d..305e640d7e 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -9342,3 +9342,27 @@ fn headless_ui_requests_stay_bounded() { let size: usize = s.ui_requests.iter().map(|r| r.to_string().len()).sum(); assert!(size < 4 << 20, "queued {size} bytes"); } + +/// `file.exportPng` writes only to `.png` paths, so automation can't replace other files with +/// page images (#3696). +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn export_png_refuses_a_path_that_is_not_png() { + let dir = scratch_dir("export-png-extension"); + let mut s = s(); + run(&mut s, "text.insert", json!({"text": "Hello"})); + for name in ["notes.txt", "config", "report.docx", "page.png.txt"] { + let target = dir.join(name); + std::fs::write(&target, b"precious user notes").unwrap(); + let r = s.run("file.exportPng", &json!({"path": target.to_string_lossy()})); + assert!(matches!(r, Err(crate::CmdError::Params(_))), "{name}: {r:?}"); + assert_eq!(std::fs::read(&target).unwrap(), b"precious user notes", "{name} was changed"); + } + // A .png path still works, whatever its case. + for name in ["page.png", "PAGE.PNG"] { + let target = dir.join(name); + s.run("file.exportPng", &json!({"path": target.to_string_lossy()})).unwrap(); + assert!(std::fs::read(&target).unwrap().starts_with(b"\x89PNG"), "{name} is not a PNG"); + } + let _ = std::fs::remove_dir_all(&dir); +}