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:
charlesmamane26-sketch authored and GitHub committed 2026-06-10 19:10:18 +00:00
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;
}