fix: rate limit /token and /revoke, bucket IPv6 by /64 and warn about untrusted forwarded headers (SEC-08)
This commit is contained in:
1 parent
782d416e46
commit
f57c237bed
7 files changed
+273
-16
No files matched your search
@@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import errno
|
||||
import ipaddress
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
@@ -30,6 +31,7 @@ from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, Response
|
||||
from starlette.routing import Route
|
||||
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
|
||||
|
||||
from aegis_gitea_mcp import __version__
|
||||
from aegis_gitea_mcp.audit import get_audit_logger
|
||||
@@ -189,6 +191,62 @@ class RateLimitMiddleware:
|
||||
await response(scope, receive, send)
|
||||
|
||||
|
||||
# Forwarded headers
|
||||
|
||||
|
||||
class ForwardedHeadersMiddleware:
|
||||
"""Applies ``X-Forwarded-For`` and ``-Proto`` from trusted peers and warns about the rest.
|
||||
|
||||
Replaces uvicorn's own proxy-header handling (the runner passes
|
||||
``proxy_headers=False``) so the original peer address is still known here:
|
||||
a request that carries ``X-Forwarded-For`` from a peer outside
|
||||
``FORWARDED_ALLOW_IPS`` is not honoured (the peer's own address is used) and
|
||||
logs one warning, because that usually means the reverse proxy is not listed
|
||||
(for example a Docker network address) and every client then shares the
|
||||
proxy's rate-limit bucket.
|
||||
|
||||
Args:
|
||||
app: The ASGI application.
|
||||
trusted: ``FORWARDED_ALLOW_IPS`` (``*`` or a comma list of IPs and networks).
|
||||
"""
|
||||
|
||||
def __init__(self, app: ASGIApp, trusted: str) -> None:
|
||||
self.trusted = trusted
|
||||
self._all = trusted.strip() == "*"
|
||||
self._networks = [
|
||||
ipaddress.ip_network(item.strip(), strict=False)
|
||||
for item in trusted.split(",")
|
||||
if item.strip() and item.strip() != "*"
|
||||
]
|
||||
self._app = ProxyHeadersMiddleware(app, trusted_hosts=trusted) # type: ignore[arg-type]
|
||||
self._warned = False
|
||||
|
||||
def _peer_trusted(self, host: str) -> bool:
|
||||
if self._all:
|
||||
return True
|
||||
try:
|
||||
address = ipaddress.ip_address(host.split("%")[0])
|
||||
except ValueError:
|
||||
return False
|
||||
return any(
|
||||
address in network for network in self._networks if network.version == address.version
|
||||
)
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
if scope["type"] == "http" and not self._warned:
|
||||
client = scope.get("client")
|
||||
has_forwarded = any(name == b"x-forwarded-for" for name, _ in scope.get("headers", []))
|
||||
if has_forwarded and client and not self._peer_trusted(client[0]):
|
||||
self._warned = True
|
||||
logger.warning(
|
||||
"a request carried X-Forwarded-For from a peer that is not in "
|
||||
"FORWARDED_ALLOW_IPS, so the header is ignored and the peer address is used "
|
||||
"as the client address; if this peer is your reverse proxy, add its address "
|
||||
"to FORWARDED_ALLOW_IPS (all clients otherwise share one rate-limit bucket)"
|
||||
)
|
||||
await self._app(scope, receive, send) # type: ignore[arg-type]
|
||||
|
||||
|
||||
# HTTP app
|
||||
|
||||
|
||||
@@ -501,12 +559,11 @@ def run_http(settings: Settings) -> int:
|
||||
if not any(isinstance(item, AccessLogQueryFilter) for item in access_logger.filters):
|
||||
access_logger.addFilter(AccessLogQueryFilter())
|
||||
uvicorn.run(
|
||||
app,
|
||||
ForwardedHeadersMiddleware(app, settings.forwarded_allow_ips),
|
||||
host=settings.mcp_host,
|
||||
port=settings.mcp_port,
|
||||
log_level=settings.log_level.lower(),
|
||||
proxy_headers=True,
|
||||
forwarded_allow_ips=settings.forwarded_allow_ips,
|
||||
proxy_headers=False, # ForwardedHeadersMiddleware applies them (and warns)
|
||||
server_header=False,
|
||||
)
|
||||
return 0
|
||||
|
||||
Reference in new issue
Block a user