Skip to content

Commit ecd35c1

Browse files
🛡️ Sentinel: [CRITICAL] Fix Rate Limiter Bypass / IP Spoofing via X-Forwarded-For (#285)
* 🛡️ Sentinel: [CRITICAL] Fix Rate Limiter Bypass / IP Spoofing via X-Forwarded-For Adds ProxyHeadersMiddleware properly scoped with TRUSTED_PROXIES to correctly extract the true client IP while avoiding trivial IP spoofing by untrusted proxies. Separated from ALLOWED_HOSTS which should only be used for TrustedHostMiddleware domain-matching. Co-authored-by: lgcorzo <46710567+lgcorzo@users.noreply.github.com> * 🛡️ Sentinel: [CRITICAL] Fix Rate Limiter Bypass / IP Spoofing via X-Forwarded-For Adds ProxyHeadersMiddleware properly scoped with TRUSTED_PROXIES to correctly extract the true client IP while avoiding trivial IP spoofing by untrusted proxies. Separated from ALLOWED_HOSTS which should only be used for TrustedHostMiddleware domain-matching. Fixes CI failures. Co-authored-by: lgcorzo <46710567+lgcorzo@users.noreply.github.com> --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent fd827ef commit ecd35c1

3 files changed

Lines changed: 66 additions & 0 deletions

File tree

.jules/sentinel.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,18 @@
5757
**Vulnerability:** The `/predict` endpoint enforced a maximum row limit but did not limit the number of columns, allowing an attacker to submit wide payloads causing memory exhaustion and Algorithmic DoS.
5858
**Learning:** When validating tabular or matrix-like data inputs, limits must be applied to all dimensions (both rows and columns) to properly bound memory usage.
5959
**Prevention:** Always define and enforce strict limits on both row and column counts for incoming data structures.
60+
61+
## 2026-12-06 - Rate Limiter Bypass / DoS via Proxy IP Masking
62+
**Vulnerability:** The in-memory rate limiter relies on `request.client.host` which yields the proxy's IP address instead of the real client IP.
63+
**Learning:** When an application is deployed behind a load balancer or reverse proxy, `request.client.host` limits all users connecting through the proxy together, enabling an attacker to inadvertently block all users or bypass rate limiting.
64+
**Prevention:** Always use `X-Forwarded-For` (or equivalent proxy headers) to extract the actual client IP for rate limiting and auditing.
65+
66+
## 2026-12-07 - Rate Limiter Bypass / IP Spoofing via X-Forwarded-For
67+
**Vulnerability:** Manually parsing the `X-Forwarded-For` header to determine the client IP allows an attacker to trivially spoof their IP address, bypassing the rate limiter and potentially causing DoS.
68+
**Learning:** Blindly trusting `X-Forwarded-For` without validating that the immediate connection comes from a trusted proxy allows IP spoofing.
69+
**Prevention:** Always use `ProxyHeadersMiddleware` (or equivalent secure middleware) configured with a strict list of trusted proxies (`trusted_hosts`) to securely extract the real client IP.
70+
71+
## 2026-12-08 - Misconfigured ProxyHeadersMiddleware (Trusting ALLOWED_HOSTS)
72+
**Vulnerability:** `ProxyHeadersMiddleware` was configured using `ALLOWED_HOSTS` instead of a separate list of trusted proxy IPs. Since `ALLOWED_HOSTS` usually contains domain names (or `*`), this causes the middleware to either fail or blindly trust all `X-Forwarded-For` headers, leading to IP Spoofing.
73+
**Learning:** `TrustedHostMiddleware` uses domain names to validate the HTTP `Host` header, whereas `ProxyHeadersMiddleware` requires the IP addresses of trusted upstream proxies to securely parse `X-Forwarded-For`. Reusing the same variable conflates these two distinct security mechanisms.
74+
**Prevention:** Always define a separate `TRUSTED_PROXIES` configuration variable (defaulting to `127.0.0.1`) specifically for `ProxyHeadersMiddleware`.

src/regression_model_template/controller/kafka_app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from fastapi import FastAPI, HTTPException, Request
1717
from fastapi.middleware.cors import CORSMiddleware
1818
from fastapi.middleware.trustedhost import TrustedHostMiddleware
19+
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
1920
from pydantic import BaseModel, field_validator
2021

2122
from regression_model_template.core.schemas import InputsSchema, Outputs
@@ -40,6 +41,7 @@
4041
# Security Configuration
4142
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(",")
4243
ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "*").split(",")
44+
TRUSTED_PROXIES = os.getenv("TRUSTED_PROXIES", "127.0.0.1").split(",")
4345

4446
# Configure logging
4547
logging.basicConfig(level=logging.INFO, format=LOGGING_FORMAT)
@@ -53,6 +55,8 @@
5355
)
5456

5557
# Security Middlewares
58+
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts=TRUSTED_PROXIES) # type: ignore[arg-type]
59+
5660
app.add_middleware(TrustedHostMiddleware, allowed_hosts=ALLOWED_HOSTS)
5761

5862
app.add_middleware(

tests/controller/test_kafka_app_dos.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,50 @@ def test_prediction_request_inconsistent_lengths():
6868
with pytest.raises(ValidationError) as excinfo:
6969
PredictionRequest(input_data=input_data)
7070
assert "All columns must have the same length" in str(excinfo.value)
71+
72+
73+
@pytest.mark.asyncio
74+
async def test_predict_rate_limiter_uses_proxy_middleware():
75+
"""Test that ProxyHeadersMiddleware correctly modifies the ASGI scope for trusted proxies."""
76+
from regression_model_template.controller.kafka_app import app
77+
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
78+
79+
proxy_middleware_config = next(m for m in app.user_middleware if m.cls == ProxyHeadersMiddleware)
80+
assert proxy_middleware_config.kwargs["trusted_hosts"] == ["127.0.0.1"]
81+
82+
async def mock_app(scope, receive, send):
83+
pass # We just want to inspect the scope after middleware
84+
85+
middleware_instance = ProxyHeadersMiddleware(app=mock_app, trusted_hosts=["10.0.0.1"])
86+
87+
# Simulate request from a trusted proxy (10.0.0.1) passing a spoofed client (203.0.113.1)
88+
scope = {
89+
"type": "http",
90+
"client": ("10.0.0.1", 12345),
91+
"headers": [(b"x-forwarded-for", b"203.0.113.1, 198.51.100.1")],
92+
}
93+
94+
async def mock_receive():
95+
return {"type": "http.request"}
96+
97+
async def mock_send(msg):
98+
pass
99+
100+
await middleware_instance(scope, mock_receive, mock_send)
101+
102+
# Assert the middleware successfully updated the client IP since it trusted the proxy
103+
# Note: Uvicorn's ProxyHeadersMiddleware sets the client IP to the right-most IP in X-Forwarded-For
104+
# that is NOT a trusted proxy. Here we only trusted 10.0.0.1, so it picked 198.51.100.1.
105+
assert scope["client"][0] == "198.51.100.1"
106+
107+
# Simulate request from an untrusted proxy (192.168.1.1)
108+
scope_untrusted = {
109+
"type": "http",
110+
"client": ("192.168.1.1", 12345),
111+
"headers": [(b"x-forwarded-for", b"203.0.113.1")],
112+
}
113+
114+
await middleware_instance(scope_untrusted, mock_receive, mock_send)
115+
116+
# Assert the middleware ignored the header and kept the untrusted IP
117+
assert scope_untrusted["client"][0] == "192.168.1.1"

0 commit comments

Comments
 (0)