diff --git a/src-tauri/src/commands/jobs.rs b/src-tauri/src/commands/jobs.rs index c0656ad..1213420 100644 --- a/src-tauri/src/commands/jobs.rs +++ b/src-tauri/src/commands/jobs.rs @@ -3,6 +3,28 @@ use crate::error::AppError; use crate::models::JobRegistry; +/// Longest job id the frontend sends (`crypto.randomUUID()` is 36 characters). +const JOB_ID_MAX: usize = 128; + +/// The webview's job id names a work folder (`/work/`) that the job deletes when it +/// ends: only `[A-Za-z0-9_-]{1,128}` is accepted, so it can never step out of the temp root. +pub(crate) fn check_job_id(job_id: &str) -> Result<(), AppError> { + let ok = !job_id.is_empty() + && job_id.len() <= JOB_ID_MAX + && job_id + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_'); + if ok { + return Ok(()); + } + Err(AppError::new( + "INVALID_JOB", + "Invalid job", + "OffPDF received a job id it does not accept.", + ) + .with_details(format!("job id of {} bytes", job_id.len()))) +} + /// Cancel a running job by id. Killing the child (if any) is handled by the /// `JobHandle`; the worker thread reaps it and returns `AppError::cancelled`. #[tauri::command] @@ -15,3 +37,37 @@ pub async fn cancel_job( } Ok(()) } + +#[cfg(test)] +mod tests { + use super::check_job_id; + + /// review-T5: a job id reaches `remove_dir_all(/work/)`. + #[test] + fn job_ids_cannot_leave_the_work_folder() { + for ok in [ + "3f2b8c1e-0d4a-4c6e-9a51-7b2f0e9d1c34", + "job-1700000000000-1a2b", + "a_b", + ] { + assert!(check_job_id(ok).is_ok(), "{ok}"); + } + let long = "a".repeat(129); + for bad in [ + "", + "..", + "../x", + "a/b", + "a\\b", + "C:x", + "a b", + long.as_str(), + "x\0", + ] { + let e = check_job_id(bad) + .err() + .unwrap_or_else(|| panic!("{bad:?} accepted")); + assert_eq!(e.code, "INVALID_JOB"); + } + } +} diff --git a/src-tauri/src/commands/pdf.rs b/src-tauri/src/commands/pdf.rs index bc1bc16..0c13757 100644 --- a/src-tauri/src/commands/pdf.rs +++ b/src-tauri/src/commands/pdf.rs @@ -43,6 +43,7 @@ pub async fn merge_pdfs( input_paths: Vec, output_path: String, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -65,6 +66,7 @@ pub async fn split_pdf( picks: Vec, mode: SplitMode, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -86,6 +88,7 @@ pub async fn assemble_pdf( output_path: String, groups: Vec, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -108,6 +111,7 @@ pub async fn edit_pdf( groups: Vec, rotations: Vec, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -130,6 +134,7 @@ pub async fn unlock_pdf( output_path: String, password: String, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -153,6 +158,7 @@ pub async fn watermark_pdf( text: String, opacity: f64, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -187,6 +193,7 @@ pub async fn crop_pdf( right: f64, bottom: f64, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -223,6 +230,7 @@ pub async fn edit_pdf_overlays( flatten_form: Option, flatten_annotations: Option, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -278,6 +286,7 @@ pub async fn stamp_pdf( color: [f64; 3], size_pct: f64, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -316,6 +325,7 @@ pub async fn poster_pdf( overlap: f64, marks: bool, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -352,6 +362,7 @@ pub async fn nup_pdf( sheet_w: f64, sheet_h: f64, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -388,6 +399,7 @@ pub async fn add_page_numbers( pad_width: Option, with_date: Option, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -430,6 +442,7 @@ pub async fn protect_pdf( user_password: String, owner_password: String, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -460,6 +473,7 @@ pub async fn extract_pages( output_path: String, pages: String, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -482,6 +496,7 @@ pub async fn delete_pages( output_path: String, pages: String, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -505,6 +520,7 @@ pub async fn rotate_pages( angle: i32, rotate_pages: String, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -535,6 +551,7 @@ pub async fn reorder_pages( output_path: String, order: String, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -559,6 +576,7 @@ pub async fn compress_pdf( quality: u32, target_bytes: Option, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -589,6 +607,7 @@ pub async fn optimize_pdf( output_path: String, groups: Vec, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); diff --git a/src-tauri/src/commands/render.rs b/src-tauri/src/commands/render.rs index ab5273a..2bfd15d 100644 --- a/src-tauri/src/commands/render.rs +++ b/src-tauri/src/commands/render.rs @@ -82,6 +82,7 @@ pub async fn pdf_to_images( format: String, dpi: u32, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -211,6 +212,7 @@ pub async fn ocr_pdf( picks: Vec, lang: String, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -258,6 +260,7 @@ pub async fn office_to_pdf_batch( output_dir: String, input_paths: Vec, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -335,6 +338,7 @@ pub async fn pdfa_pdf( output_path: String, groups: Vec, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -394,6 +398,7 @@ pub async fn detect_blank_pages( input_path: String, sensitivity: String, ) -> Result, AppError> { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let jid = job_id.clone(); let res = tauri::async_runtime::spawn_blocking(move || { @@ -431,6 +436,7 @@ pub async fn write_pdf_meta( fields: crate::pdf_engine::metadata::PdfMeta, clear_all: bool, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -468,6 +474,7 @@ pub async fn export_pdf_text( first_page: Option, last_page: Option, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone(); @@ -504,6 +511,7 @@ pub async fn pdf_to_office( groups: Vec, format: String, ) -> Result { + super::jobs::check_job_id(&job_id)?; let handle = registry.register(&job_id); let app2 = app.clone(); let jid = job_id.clone();