Security: cap MCP HTTP request bodies and add optional Origin allowlist (#280)
Reject request bodies above 16 MiB with 413 before allocating, stopping an unauthenticated Content-Length OOM (vec![0u8; len]). Add an optional, default-off OBSCURA_MCP_ALLOWED_ORIGINS allowlist; unset keeps the permissive default so issue #175 hosted dashboards are unaffected.
This commit is contained in:
1 parent
dd7395a80a
commit
6c9d5c55ad
2 files changed
+141
No files matched your search
@@ -84,3 +84,54 @@ async fn options_preflight_lists_required_browser_headers() {
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Regression test for the unbounded `Content-Length` allocation: a POST that
|
||||
/// advertises a huge body must be rejected with 413 *before* the server tries
|
||||
/// to allocate `vec![0u8; len]`, rather than committing gigabytes of RAM and
|
||||
/// OOM-ing the process (unauthenticated DoS).
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn oversized_content_length_is_rejected() {
|
||||
let port = pick_free_port();
|
||||
let local = LocalSet::new();
|
||||
|
||||
let server = local.spawn_local(async move {
|
||||
let _ = obscura_mcp::http::run("127.0.0.1".to_string(), port, None, None, false).await;
|
||||
});
|
||||
|
||||
local
|
||||
.run_until(async {
|
||||
for _ in 0..40 {
|
||||
if TcpStream::connect(("127.0.0.1", port)).await.is_ok() {
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
|
||||
let mut stream = TcpStream::connect(("127.0.0.1", port))
|
||||
.await
|
||||
.expect("MCP server did not come up");
|
||||
// 8 GiB advertised, zero body sent. Pre-fix the server allocates 8 GiB.
|
||||
let req = b"POST /mcp HTTP/1.1\r\n\
|
||||
Host: 127.0.0.1\r\n\
|
||||
Content-Type: application/json\r\n\
|
||||
Content-Length: 8589934592\r\n\
|
||||
\r\n";
|
||||
stream.write_all(req).await.unwrap();
|
||||
stream.flush().await.unwrap();
|
||||
|
||||
let mut buf = [0u8; 1024];
|
||||
let n = timeout(Duration::from_secs(2), stream.read(&mut buf))
|
||||
.await
|
||||
.expect("read timed out")
|
||||
.expect("read failed");
|
||||
let response = String::from_utf8_lossy(&buf[..n]).to_string();
|
||||
|
||||
server.abort();
|
||||
|
||||
assert!(
|
||||
response.starts_with("HTTP/1.1 413"),
|
||||
"expected 413 Payload Too Large, got:\n{response}"
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Reference in new issue
Block a user