web host mode: refuse an oversized host file before copying it, cap the name, and document trust and limits

This commit is contained in:
Helen Kwok committed 2026-11-05 17:03:44 +08:00
1 parent d628abb7b0
commit bf6bee500a
2 files changed
+23 -5

No files matched your search

+17 -5
View File
@@ -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<u8>)> {
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<String> {
/// `bytes` as an `ArrayBuffer` or `Uint8Array`.
fn bytes(data: &JsValue) -> Option<Vec<u8>> {
let bytes = js_sys::Reflect::get(data, &JsValue::from_str("bytes")).ok()?;
if let Some(array) = bytes.dyn_ref::<js_sys::Uint8Array>() {
Some(array.to_vec())
let view = if let Some(array) = bytes.dyn_ref::<js_sys::Uint8Array>() {
array.clone()
} else if bytes.is_instance_of::<js_sys::ArrayBuffer>() {
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.
+6
View File
@@ -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