From f29543f4296fa230a4d5a86c6c8ba2760931ca9b Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Mon, 2 Feb 2026 10:50:19 +0100 Subject: [PATCH] wasm: harden host api input validation --- backend/src/plugins/wasm/host_api.rs | 61 +++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/backend/src/plugins/wasm/host_api.rs b/backend/src/plugins/wasm/host_api.rs index 49d5770..bd59ee7 100644 --- a/backend/src/plugins/wasm/host_api.rs +++ b/backend/src/plugins/wasm/host_api.rs @@ -25,6 +25,13 @@ pub const CAP_SETTINGS: &str = "settings"; /// Capability identifier for outbound HTTP requests. pub const CAP_OUTBOUND_HTTP: &str = "outbound_http"; +const MAX_WASM_STRING_BYTES: u32 = 64 * 1024; +const MAX_WASM_HTTP_BODY_BYTES: u32 = 256 * 1024; +const ERR_TOO_LARGE: u32 = 9; +const ERR_INVALID_URL: u32 = 10; +const ERR_INVALID_METHOD: u32 = 11; +const ERR_INVALID_BODY_UTF8: u32 = 12; + /// Plugin capability configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Capability { @@ -92,6 +99,9 @@ impl HostState { return true; } if let Ok(parsed) = reqwest::Url::parse(url) { + if parsed.scheme() != "http" && parsed.scheme() != "https" { + return false; + } if let Some(host) = parsed.host_str() { return self.egress_allowlist.iter().any(|pattern| { if pattern.starts_with("*.") { @@ -107,6 +117,13 @@ impl HostState { } } +fn is_valid_method(method: &str) -> bool { + matches!( + method, + "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS" + ) +} + /// Combined state with resource limits for WASM execution. pub struct HostStateWithLimits { pub inner: HostState, @@ -147,6 +164,9 @@ pub fn register_host_functions(linker: &mut Linker) -> Resu .func_wrap("env", "host_log", |mut caller: wasmtime::Caller<'_, HostStateWithLimits>, ptr: u32, len: u32, level: u32| { let memory = caller.get_export("memory").and_then(|e| e.into_memory()); if let Some(mem) = memory { + if len > MAX_WASM_STRING_BYTES { + return; + } let mut buf = vec![0u8; len as usize]; if mem.read(&caller, ptr as usize, &mut buf).is_ok() { if let Ok(msg) = String::from_utf8(buf) { @@ -175,6 +195,10 @@ pub fn register_host_functions(linker: &mut Linker) -> Resu None => return pack_result(1, 0), }; + if key_len > MAX_WASM_STRING_BYTES { + return pack_result(ERR_TOO_LARGE, 0); + } + let mut key_buf = vec![0u8; key_len as usize]; if memory.read(&caller, key_ptr as usize, &mut key_buf).is_err() { return pack_result(2, 0); @@ -249,6 +273,10 @@ pub fn register_host_functions(linker: &mut Linker) -> Resu None => return pack_result(1, 0), }; + if key_len > MAX_WASM_STRING_BYTES { + return pack_result(ERR_TOO_LARGE, 0); + } + let mut key_buf = vec![0u8; key_len as usize]; if memory.read(&caller, key_ptr as usize, &mut key_buf).is_err() { return pack_result(2, 0); @@ -299,6 +327,10 @@ pub fn register_host_functions(linker: &mut Linker) -> Resu None => return 1, }; + if key_len > MAX_WASM_STRING_BYTES || val_len > MAX_WASM_STRING_BYTES { + return ERR_TOO_LARGE; + } + let mut key_buf = vec![0u8; key_len as usize]; let mut val_buf = vec![0u8; val_len as usize]; @@ -361,6 +393,10 @@ pub fn register_host_functions(linker: &mut Linker) -> Resu None => return 1, }; + if event_len > MAX_WASM_STRING_BYTES || payload_len > MAX_WASM_STRING_BYTES { + return ERR_TOO_LARGE; + } + let mut event_buf = vec![0u8; event_len as usize]; let mut payload_buf = vec![0u8; payload_len as usize]; @@ -461,6 +497,10 @@ pub fn register_host_functions(linker: &mut Linker) -> Resu None => return pack_result(1, 0), }; + if url_len > MAX_WASM_STRING_BYTES || method_len > MAX_WASM_STRING_BYTES || body_len > MAX_WASM_HTTP_BODY_BYTES { + return pack_result(ERR_TOO_LARGE, 0); + } + let mut url_buf = vec![0u8; url_len as usize]; let mut method_buf = vec![0u8; method_len as usize]; @@ -486,7 +526,22 @@ pub fn register_host_functions(linker: &mut Linker) -> Resu Err(_) => return pack_result(5, 0), }; - let body = String::from_utf8(body_buf).unwrap_or_default(); + let Ok(parsed_url) = reqwest::Url::parse(&url) else { + return pack_result(ERR_INVALID_URL, 0); + }; + if parsed_url.scheme() != "http" && parsed_url.scheme() != "https" { + return pack_result(ERR_INVALID_URL, 0); + } + + let method_upper = method.to_ascii_uppercase(); + if !is_valid_method(method_upper.as_str()) { + return pack_result(ERR_INVALID_METHOD, 0); + } + + let body = match String::from_utf8(body_buf) { + Ok(b) => b, + Err(_) => return pack_result(ERR_INVALID_BODY_UTF8, 0), + }; let state = caller.data(); @@ -511,11 +566,13 @@ pub fn register_host_functions(linker: &mut Linker) -> Resu return serde_json::json!({"error": "client_init_failed"}); }; - let mut req = match method.to_ascii_uppercase().as_str() { + let mut req = match method_upper.as_str() { "POST" => client.post(&url), "PUT" => client.put(&url), "DELETE" => client.delete(&url), "PATCH" => client.patch(&url), + "HEAD" => client.head(&url), + "OPTIONS" => client.request(reqwest::Method::OPTIONS, &url), _ => client.get(&url), };