diff --git a/apps/gridcraft-web/src/host.rs b/apps/gridcraft-web/src/host.rs index 8130d2682..0ab4fefbb 100644 --- a/apps/gridcraft-web/src/host.rs +++ b/apps/gridcraft-web/src/host.rs @@ -45,6 +45,12 @@ use crate::hostmode::open_name; /// counts as saved under that name, as it would after Save on the desktop. const WORKBOOK_EXTS: &[&str] = &["xlsx"]; +/// The largest file the host may hand over to open, checked before it is copied (the app's own readers have their own limits). +const MAX_OPEN_BYTES: u64 = 512 << 20; + +/// The longest file name taken from the host. +const MAX_NAME_CHARS: usize = 255; + /// The longest user name taken from `?author=`. const MAX_AUTHOR_CHARS: usize = 200; @@ -219,7 +225,7 @@ fn deliver_open(inbox: &Inbox, ctx: &egui::Context, data: &JsValue) { /// The file an open message or event carries: a usable name and the bytes. fn open_payload(data: &JsValue) -> Option<(String, Vec)> { - let name = open_name(string(data, "name").as_deref()); + let name: String = open_name(string(data, "name").as_deref()).chars().take(MAX_NAME_CHARS).collect(); let bytes = bytes(data)?; Some((name, bytes)) } @@ -249,13 +255,19 @@ fn string(data: &JsValue, key: &str) -> Option { /// `bytes` as an `ArrayBuffer` or `Uint8Array`. fn bytes(data: &JsValue) -> Option> { let bytes = js_sys::Reflect::get(data, &JsValue::from_str("bytes")).ok()?; - if let Some(array) = bytes.dyn_ref::() { - Some(array.to_vec()) + let view = if let Some(array) = bytes.dyn_ref::() { + array.clone() } else if bytes.is_instance_of::() { - Some(js_sys::Uint8Array::new(&bytes).to_vec()) + js_sys::Uint8Array::new(&bytes) } else { - None + return None; + }; + // A file from the host is untrusted input: refuse an oversized one before copying it into the app's memory. + if u64::from(view.length()) > MAX_OPEN_BYTES { + log::warn!("host file is {} bytes, over the {} MiB host open limit; not opened", view.length(), MAX_OPEN_BYTES >> 20); + return None; } + Some(view.to_vec()) } /// A `JsValue` error as a string, after `what` went wrong. diff --git a/packaging/web/README.md b/packaging/web/README.md index 46a54842d..d4e85a765 100644 --- a/packaging/web/README.md +++ b/packaging/web/README.md @@ -149,6 +149,12 @@ As with the parent bridge, the app starts with an empty workbook instead of a sa must be exactly `page`; anything else is ignored, and the app runs as a standalone page with downloads. +### Trust and limits + +- **Who is trusted.** In page host mode the page and GridCraft share one document, so any script on the page can drive the app: open files in it and receive what it saves. Use this mode only for a page you control. The iframe mode (`?host=parent`) answers only the parent window, on the page's own origin. +- **Files from the host are untrusted input.** An opened file goes through the same reading as one picked with File ▸ Open or dropped on the window, with the same format limits; before it is copied into the app's memory, its size is checked against the host open limit (512 MiB) and a larger file is refused, and a name is cut to 255 characters. +- **The event and message names are the interface.** `gridcraft:ready`, `gridcraft:open`, `gridcraft:save`, `gridcraft:dirty` and `gridcraft:failed`, and the fields they carry (the same as the `?host=parent` messages), stay stable; changing one is a breaking change. + ## Renderer selection GridCraft's web build is the same egui app as the desktop one, compiled to wasm