Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@
**Vulnerability:** The `validate_model_request` function bypassed directory checks entirely when `allowed_model_directories` was empty. This allowed attackers to specify absolute paths (e.g., `/etc/passwd.gguf`) and bypass path restrictions.
**Learning:** Checking for `..` and `~` is insufficient for path safety because absolute paths don't contain them. When an allowlist is intentionally empty, it is critical to explicitly deny absolute paths or deny all requests, rather than allowing any path.
**Prevention:** Explicitly deny absolute paths if no specific allowed directories are configured.

## 2024-06-23 - [Unbounded Payload DoS via Chunked Transfer Encoding]
**Vulnerability:** The request sanitization middleware relied on the `content-length` header to limit payload sizes but failed to handle requests with `transfer-encoding: chunked` missing a content length, leaving the server vulnerable to unbounded payload DoS attacks.
**Learning:** Checking the `content-length` header is insufficient to prevent unbounded payload DoS attacks. You must also explicitly reject requests that use chunked transfer encoding without a defined content length.
**Prevention:** Explicitly reject requests with `transfer-encoding: chunked` if they lack a defined content length in middleware.
10 changes: 10 additions & 0 deletions crates/bitnet-server/src/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,16 @@
// For inference requests, we'll validate in the handler
// This middleware focuses on general request sanitization

// πŸ›‘οΈ Sentinel: Reject unbounded chunked payloads to prevent DoS
if !request.headers().contains_key("content-length")

Check notice on line 497 in crates/bitnet-server/src/security.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR exposed

| Expression: if !request.headers().contains_key("content-length") | Confidence: 1.0
&& let Some(te) = request.headers().get("transfer-encoding")

Check warning on line 498 in crates/bitnet-server/src/security.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR infection_unknown

Add a targeted boundary or negative-path test, or teach ripr about the fixture/builder in ripr.toml. | Expression: && let Some(te) = request.headers().get("transfer-encoding") | Confidence: 0.66

Check notice on line 498 in crates/bitnet-server/src/security.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR exposed

| Expression: && let Some(te) = request.headers().get("transfer-encoding") | Confidence: 1.0
&& let Ok(te_str) = te.to_str()

Check warning on line 499 in crates/bitnet-server/src/security.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR infection_unknown

Add a targeted boundary or negative-path test, or teach ripr about the fixture/builder in ripr.toml. | Expression: && let Ok(te_str) = te.to_str() | Confidence: 0.66

Check notice on line 499 in crates/bitnet-server/src/security.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR exposed

| Expression: && let Ok(te_str) = te.to_str() | Confidence: 1.0
&& te_str.to_lowercase().contains("chunked")

Check warning on line 500 in crates/bitnet-server/src/security.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR infection_unknown

Add a targeted boundary or negative-path test, or teach ripr about the fixture/builder in ripr.toml. | Expression: && te_str.to_lowercase().contains("chunked") | Confidence: 0.66

Check notice on line 500 in crates/bitnet-server/src/security.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR exposed

| Expression: && te_str.to_lowercase().contains("chunked") | Confidence: 1.0
{

Check notice on line 501 in crates/bitnet-server/src/security.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR static_unknown

Escalate to real mutation testing or deep static analysis for this probe. | Expression: { | Confidence: 0.5
warn!("Rejected chunked request without content-length");

Check notice on line 502 in crates/bitnet-server/src/security.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR exposed

| Expression: warn!("Rejected chunked request without content-length"); | Confidence: 1.0

Check notice on line 502 in crates/bitnet-server/src/security.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR exposed

| Expression: warn!("Rejected chunked request without content-length"); | Confidence: 1.0
return Err(StatusCode::LENGTH_REQUIRED);

Check notice on line 503 in crates/bitnet-server/src/security.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR exposed

| Expression: return Err(StatusCode::LENGTH_REQUIRED); | Confidence: 1.0

Check notice on line 503 in crates/bitnet-server/src/security.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR exposed

| Expression: return Err(StatusCode::LENGTH_REQUIRED); | Confidence: 1.0

Check notice on line 503 in crates/bitnet-server/src/security.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR exposed

| Expression: return Err(StatusCode::LENGTH_REQUIRED); | Confidence: 1.0
}
Comment on lines +497 to +504

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

According to the HTTP/1.1 specification (RFC 9112, Section 6.2), a sender must not send a Content-Length header in any message that contains a Transfer-Encoding header.

By rejecting chunked requests that lack a Content-Length header, this middleware will reject all standard-compliant chunked requests, effectively breaking chunked transfer encoding support entirely.

If the goal is to prevent unbounded payloads:

  1. If chunked transfer encoding is not supported/allowed: Reject all chunked requests outright, regardless of the presence of a Content-Length header.
  2. If chunked transfer encoding is supported: Enforce a maximum body size limit on the request stream itself (e.g., using tower_http::limit::RequestBodyLimitLayer or Axum's DefaultBodyLimit), rather than relying on headers which can be omitted or spoofed.

If you wish to reject all chunked requests to prevent DoS, you should remove the content-length check.

    if let Some(te) = request.headers().get("transfer-encoding")
        && let Ok(te_str) = te.to_str()
        && te_str.to_lowercase().contains("chunked")
    {
        warn!("Rejected chunked request: chunked transfer encoding is not supported");
        return Err(StatusCode::BAD_REQUEST);
    }

Comment on lines +497 to +504

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Requiring a content-length header for requests with transfer-encoding: chunked violates the HTTP/1.1 specification (RFC 9112, Section 6.2), which states that a sender MUST NOT send a Content-Length header if Transfer-Encoding is present. Standard-compliant clients sending chunked requests will be broken by this change.

Additionally, allowing chunked requests only when content-length is present introduces a risk of HTTP Request Smuggling, as different proxies/servers handle the conflict between these two headers differently.

Recommended Approaches:

  1. If chunked requests are not supported: Reject all chunked requests outright without checking for content-length.
  2. If chunked requests are supported: Enforce a limit on the request body stream itself (e.g., using Axum's DefaultBodyLimit or wrapping the body in a size-limiting stream like http_body_util::Limited) rather than relying on header validation in this middleware.
    if let Some(te) = request.headers().get("transfer-encoding")
        && let Ok(te_str) = te.to_str()
        && te_str.to_lowercase().contains("chunked")
    {
        warn!("Rejected chunked request to prevent unbounded payload DoS");
        return Err(StatusCode::BAD_REQUEST);
    }


// Check request size
if let Some(content_length) = request.headers().get("content-length")
&& let Ok(length_str) = content_length.to_str()
Expand Down
Binary file added test_chunked
Binary file not shown.
Loading