Skip to content

feat(ai-service): enforce HTTP request body size limit (fixes #137)#192

Merged
kilodesodiq-arch merged 1 commit into
ChainForgee:mainfrom
gbengaeben:fix/issue-137-request-body-size-limit
Jun 22, 2026
Merged

feat(ai-service): enforce HTTP request body size limit (fixes #137)#192
kilodesodiq-arch merged 1 commit into
ChainForgee:mainfrom
gbengaeben:fix/issue-137-request-body-size-limit

Conversation

@gbengaeben

Copy link
Copy Markdown
Contributor

Summary

Closes #137. Adds a configurable HTTP request body size limit to the AI service to mitigate the memory-exhaustion DoS vector flagged in the security review.

The new MaxRequestBodySizeMiddleware is mounted as the outermost raw ASGI middleware on the FastAPI app, so oversized payloads are rejected before any other middleware, route handler, or framework code can buffer the body. Both attack paths are covered:

  • Header spoofing (malicious Content-Length larger than the cap) → eager 413 before any bytes are read off the wire.
  • Chunked transfer smuggling (no Content-Length, body streams past the cap) → bytes counted via a wrapped receive callable; first over-cap chunk raises an internal HTTPBodyTooLarge signal that is caught and converted into a 413 response.

413 responses use the project's ErrorEnvelope shape (same contract as every other handler) and include precise wording that distinguishes declared-size from streamed-size rejection. Each rejection emits a structured logger.warning carrying method, path, byte count, limit, client IP, and rejection reason so ops/SIEM can correlate attack patterns.

Files

File Change
app/ai-service/config.py New max_request_body_bytes (default 10485760 = 10 MiB) and request_body_bypass_paths settings, with env-var docstring describing exact-match vs. trailing-slash prefix semantics.
app/ai-service/main.py Added HTTPBodyTooLarge exception and MaxRequestBodySizeMiddleware class, wired via app.add_middleware(...). Health/docs/metrics paths are always bypassed. Disabled when max_request_body_bytes <= 0.
app/ai-service/README.md Env-table rows for the two new variables, including the trailing-slash prefix note and the 0 disables caveat.
app/ai-service/tests/test_request_body_limit.py 17 new pytest cases (see below).

Tests

$ pytest app/ai-service/tests/test_request_body_limit.py -v
17 passed

Coverage:

  • Eager Content-Length rejection (above and within the cap) ✅
  • Streaming chunked rejection via raw ASGI scope driving (below, exactly-at, and above the cap, single-chunk and multi-chunk) ✅
  • Malformed Content-Length falls through cleanly without crashing ✅
  • GET / HEAD are not subject to the limit ✅
  • /health and other infrastructure paths are bypassed regardless of cap ✅
  • Custom bypass_prefixes work for both exact match and /prefix/ match ✅
  • max_request_body_bytes=0 disables the middleware cleanly ✅
  • 413 response body matches the project's ErrorEnvelope shape, with the correct wording for the streamed vs declared branches ✅
  • Real main.app is wired with the expected 10 MiB cap (wiring test) ✅
  • Lowered-cap end-to-end (TestClient with lowered settings → oversized POST) returns 413 ✅
  • Lowered-cap end-to-end with fraudulent Content-Length → 413 ✅

Regression check on the files my middleware touches (test_main.py, tests/test_versioned_routes.py): 0 regressions. (There are 8 pre-existing failures in test_ocr.py, test_preprocessing.py, and tests/test_routes.py::TestHealthDependenciesEndpoint, all of which fail on unmodified main and are unrelated to this change — verified by git stash + retest.)

Configuration

Env var Default Notes
MAX_REQUEST_BODY_BYTES 10485760 (10 MiB) Cap in bytes. 0 disables (not recommended in production).
REQUEST_BODY_BYPASS_PATHS empty Comma-separated list. Entries without a trailing '/' must match the path exactly; entries with a trailing '/' match any path with that prefix. The defaults (/health, /, /ai/metrics, /docs, /redoc, /openapi.json) are always merged in.

Security & ops notes

  • Two real bugs were caught during local testing and fixed before commit:
    1. _is_bypassed was treating / (the root discovery endpoint) as a prefix wildcard; since every URL starts with /, every request was being bypassed. Fixed by excluding exactly / from prefix matching while keeping its exact-match behavior.
    2. The eager return await self._send_413(...) was mis-indented and would have fired on every Content-Length-bearing request once bug REPO-001 [CRITICAL] Remove hardcoded encryption fallback key #1 was fixed. Re-indented so it only fires inside if declared > self.max_bytes:.
  • HTTP request paths are limited to POST, PUT, PATCH — the only methods with bodies. GET, HEAD, OPTIONS, DELETE, etc. are passed through untouched.
  • DoS protection at the wire boundary: the 413 is emitted without buffering the offending body, so memory pressure is bounded by MAX_REQUEST_BODY_BYTES per request rather than by the size of the largest body the server has ever buffered.

Test commands

cd app/ai-service
pip install -r requirements.txt
pytest tests/test_request_body_limit.py -v
pytest tests/test_versioned_routes.py test_main.py -q   # regression

…rgee#137)

Adds MaxRequestBodySizeMiddleware as the outermost raw ASGI middleware on
the FastAPI app, rejecting oversized POST/PUT/PATCH payloads with HTTP 413
before the body is read into memory.

- New MAX_REQUEST_BODY_BYTES setting (default 10485760 = 10 MiB) driven by
  pydantic-settings env var; REQUEST_BODY_BYPASS_PATHS lets operators opt
  specific endpoints out of the limit (exact match by default, trailing '/'
  for prefix match). Health/docs/metrics are always bypassed.
- Eager Content-Length short-circuits with a 413 if the declared size
  exceeds the cap; an HTTPBodyTooLarge signal from the receive-wrap stream
  counter is converted into a 413 once the cap is breached on chunked
  bodies. Path is bypass at the same level as monitor_requests' NEVER
  throttle list.
- 413 responses use the project's ErrorEnvelope shape for consistency with
  every other error handler and include precise wording distinguishing
  declared-size from streamed-size rejection. Each rejection logs a
  structured warning (reason=declared_size|streamed_size) for ops/SIEM
  correlation.
- 17 new pytest cases cover eager, streamed, malformed-Content-Length,
  GET/HEAD passthrough, bypass exact/prefix, disabled limit, and real
  main.app wiring (full suite: 17 new pass, 0 regressions in the affected
  files).

Closes ChainForgee#137

Copy link
Copy Markdown
Contributor

Thanks @gbengaeben — clean implementation of the body-size middleware. AI Service CI is green and the change targets the DoS surface mentioned in #137. Merging now.

@kilodesodiq-arch kilodesodiq-arch merged commit 2c9a0ae into ChainForgee:main Jun 22, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[MEDIUM] AI service has no request body size limit

2 participants