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
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@
**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.
## 2025-10-25 - Chunked Encoding Request Smuggling
**Vulnerability:** The Axum middleware relied solely on checking the `content-length` header to enforce request body size limits. This allowed an attacker to bypass the limit using chunked encoding (`Transfer-Encoding: chunked`), potentially leading to request smuggling and DoS via memory exhaustion.
**Learning:** Manually reading headers like `content-length` is insufficient for enforcing HTTP limits since standard HTTP clients can utilize chunked encoding which obscures the total size until fully consumed.
**Prevention:** Always enforce global body limits directly through the web framework's native capabilities (e.g., Axum's `DefaultBodyLimit`), which correctly handles all encoding types. When writing custom sanitization middleware, explicitly reject ambiguous or complex encodings if they aren't strictly required.
25 changes: 2 additions & 23 deletions crates/bitnet-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,10 +678,8 @@
// Add comprehensive middleware stack
app = app
.layer(middleware::from_fn(security_headers_middleware))
.layer(middleware::from_fn_with_state(
self.security_validator.clone(),
request_validation_middleware,
))
// πŸ›‘οΈ Sentinel: Enforce global body limit to protect against chunked request smuggling

Check notice on line 681 in crates/bitnet-server/src/lib.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

Check notice on line 681 in crates/bitnet-server/src/lib.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: request_validation_middleware, | Confidence: 0.5

Check notice on line 681 in crates/bitnet-server/src/lib.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: .layer(middleware::from_fn_with_state( | Confidence: 0.5
.layer(axum::extract::DefaultBodyLimit::max(self.config.security.max_prompt_length * 2))

Check notice on line 682 in crates/bitnet-server/src/lib.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR exposed

| Expression: .layer(axum::extract::DefaultBodyLimit::max(self.config.security.max_prompt_length * 2)) | Confidence: 1.0
.layer(middleware::from_fn_with_state(
self.config.security.clone(),
security::ip_blocking_middleware,
Expand Down Expand Up @@ -1942,26 +1940,7 @@
response
}

/// Request validation middleware
async fn request_validation_middleware(
State(validator): State<Arc<SecurityValidator>>,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
// Check request size limits
if let Some(content_length) = request.headers().get("content-length")
&& let Ok(length_str) = content_length.to_str()
&& let Ok(length) = length_str.parse::<usize>()
&& length > validator.config().max_prompt_length * 2
{
warn!(content_length = length, "Request payload too large");
return Err(StatusCode::PAYLOAD_TOO_LARGE);
}

Ok(next.run(request).await)
}

/// Utility functions

Check notice on line 1943 in crates/bitnet-server/src/lib.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.2

Check notice on line 1943 in crates/bitnet-server/src/lib.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.2

Check notice on line 1943 in crates/bitnet-server/src/lib.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.2

Check notice on line 1943 in crates/bitnet-server/src/lib.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: ) -> Result<Response, StatusCode> { | Confidence: 0.2

Check notice on line 1943 in crates/bitnet-server/src/lib.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: async fn request_validation_middleware( | Confidence: 0.2

Check warning on line 1943 in crates/bitnet-server/src/lib.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR no_static_path

Add a co-located test that reaches and observes the changed owner so a discriminator exists; ripr found no static test path for this change. | Expression: State(validator): State<Arc<SecurityValidator>>, | Confidence: 0.48

Check warning on line 1943 in crates/bitnet-server/src/lib.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR no_static_path

Add a co-located test that reaches and observes the changed owner so a discriminator exists; ripr found no static test path for this change. | Expression: return Err(StatusCode::PAYLOAD_TOO_LARGE); | Confidence: 0.48

Check warning on line 1943 in crates/bitnet-server/src/lib.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR no_static_path

Add a co-located test that reaches and observes the changed owner so a discriminator exists; ripr found no static test path for this change. | Expression: Ok(next.run(request).await) | Confidence: 0.48

Check warning on line 1943 in crates/bitnet-server/src/lib.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR no_static_path

Add a co-located test that reaches and observes the changed owner so a discriminator exists; ripr found no static test path for this change. | Expression: return Err(StatusCode::PAYLOAD_TOO_LARGE); | Confidence: 0.48

Check warning on line 1943 in crates/bitnet-server/src/lib.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR no_static_path

Add a co-located test that reaches and observes the changed owner so a discriminator exists; ripr found no static test path for this change. | Expression: warn!(content_length = length, "Request payload too large"); | Confidence: 0.48

Check warning on line 1943 in crates/bitnet-server/src/lib.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR no_static_path

Add a co-located test that reaches and observes the changed owner so a discriminator exists; ripr found no static test path for this change. | Expression: && length > validator.config().max_prompt_length * 2 | Confidence: 0.48

Check warning on line 1943 in crates/bitnet-server/src/lib.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR no_static_path

Add a co-located test that reaches and observes the changed owner so a discriminator exists; ripr found no static test path for this change. | Expression: && let Ok(length) = length_str.parse::<usize>() | Confidence: 0.48

Check warning on line 1943 in crates/bitnet-server/src/lib.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR no_static_path

Add a co-located test that reaches and observes the changed owner so a discriminator exists; ripr found no static test path for this change. | Expression: && let Ok(length_str) = content_length.to_str() | Confidence: 0.48

Check warning on line 1943 in crates/bitnet-server/src/lib.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR no_static_path

Add a co-located test that reaches and observes the changed owner so a discriminator exists; ripr found no static test path for this change. | Expression: if let Some(content_length) = request.headers().get("content-length") | Confidence: 0.48

Check warning on line 1943 in crates/bitnet-server/src/lib.rs

View workflow job for this annotation

GitHub Actions / ripr

RIPR no_static_path

Add a co-located test that reaches and observes the changed owner so a discriminator exists; ripr found no static test path for this change. | Expression: State(validator): State<Arc<SecurityValidator>>, | Confidence: 0.48
/// Calculate tokens per second from token count and duration
fn calculate_tokens_per_second(tokens: u64, duration: Duration) -> f64 {
let duration_ms = duration.as_millis();
Expand Down
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 chunked requests to prevent bypasses of our size checks,
// avoiding HTTP desync / request smuggling vulnerabilities.
if let Some(te) = request.headers().get("transfer-encoding")

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

View workflow job for this annotation

GitHub Actions / ripr

RIPR exposed

| Expression: if let Some(te) = request.headers().get("transfer-encoding") | Confidence: 1.0
&& let Ok(te_str) = te.to_str()
&& te_str.contains("chunked")
{
warn!("Chunked requests are not permitted");
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
4 changes: 4 additions & 0 deletions test_body_limit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
use axum::{Router, extract::DefaultBodyLimit};
fn test() {
let app: Router = Router::new().layer(DefaultBodyLimit::max(1024));
}
1 change: 1 addition & 0 deletions test_chunked.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn test() {}
Loading