wasm: harden host api input validation

This commit is contained in:
Marco Allegretti 2026-02-02 10:50:19 +01:00
parent 2fa1ac5fdf
commit f29543f429

View file

@ -25,6 +25,13 @@ pub const CAP_SETTINGS: &str = "settings";
/// Capability identifier for outbound HTTP requests. /// Capability identifier for outbound HTTP requests.
pub const CAP_OUTBOUND_HTTP: &str = "outbound_http"; 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. /// Plugin capability configuration.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Capability { pub struct Capability {
@ -92,6 +99,9 @@ impl HostState {
return true; return true;
} }
if let Ok(parsed) = reqwest::Url::parse(url) { if let Ok(parsed) = reqwest::Url::parse(url) {
if parsed.scheme() != "http" && parsed.scheme() != "https" {
return false;
}
if let Some(host) = parsed.host_str() { if let Some(host) = parsed.host_str() {
return self.egress_allowlist.iter().any(|pattern| { return self.egress_allowlist.iter().any(|pattern| {
if pattern.starts_with("*.") { 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. /// Combined state with resource limits for WASM execution.
pub struct HostStateWithLimits { pub struct HostStateWithLimits {
pub inner: HostState, pub inner: HostState,
@ -147,6 +164,9 @@ pub fn register_host_functions(linker: &mut Linker<HostStateWithLimits>) -> Resu
.func_wrap("env", "host_log", |mut caller: wasmtime::Caller<'_, HostStateWithLimits>, ptr: u32, len: u32, level: u32| { .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()); let memory = caller.get_export("memory").and_then(|e| e.into_memory());
if let Some(mem) = memory { if let Some(mem) = memory {
if len > MAX_WASM_STRING_BYTES {
return;
}
let mut buf = vec![0u8; len as usize]; let mut buf = vec![0u8; len as usize];
if mem.read(&caller, ptr as usize, &mut buf).is_ok() { if mem.read(&caller, ptr as usize, &mut buf).is_ok() {
if let Ok(msg) = String::from_utf8(buf) { if let Ok(msg) = String::from_utf8(buf) {
@ -175,6 +195,10 @@ pub fn register_host_functions(linker: &mut Linker<HostStateWithLimits>) -> Resu
None => return pack_result(1, 0), 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]; let mut key_buf = vec![0u8; key_len as usize];
if memory.read(&caller, key_ptr as usize, &mut key_buf).is_err() { if memory.read(&caller, key_ptr as usize, &mut key_buf).is_err() {
return pack_result(2, 0); return pack_result(2, 0);
@ -249,6 +273,10 @@ pub fn register_host_functions(linker: &mut Linker<HostStateWithLimits>) -> Resu
None => return pack_result(1, 0), 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]; let mut key_buf = vec![0u8; key_len as usize];
if memory.read(&caller, key_ptr as usize, &mut key_buf).is_err() { if memory.read(&caller, key_ptr as usize, &mut key_buf).is_err() {
return pack_result(2, 0); return pack_result(2, 0);
@ -299,6 +327,10 @@ pub fn register_host_functions(linker: &mut Linker<HostStateWithLimits>) -> Resu
None => return 1, 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 key_buf = vec![0u8; key_len as usize];
let mut val_buf = vec![0u8; val_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<HostStateWithLimits>) -> Resu
None => return 1, 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 event_buf = vec![0u8; event_len as usize];
let mut payload_buf = vec![0u8; payload_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<HostStateWithLimits>) -> Resu
None => return pack_result(1, 0), 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 url_buf = vec![0u8; url_len as usize];
let mut method_buf = vec![0u8; method_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<HostStateWithLimits>) -> Resu
Err(_) => return pack_result(5, 0), 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(); let state = caller.data();
@ -511,11 +566,13 @@ pub fn register_host_functions(linker: &mut Linker<HostStateWithLimits>) -> Resu
return serde_json::json!({"error": "client_init_failed"}); 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), "POST" => client.post(&url),
"PUT" => client.put(&url), "PUT" => client.put(&url),
"DELETE" => client.delete(&url), "DELETE" => client.delete(&url),
"PATCH" => client.patch(&url), "PATCH" => client.patch(&url),
"HEAD" => client.head(&url),
"OPTIONS" => client.request(reqwest::Method::OPTIONS, &url),
_ => client.get(&url), _ => client.get(&url),
}; };