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
@@ -50,7 +50,7 @@ when OAuth is on.
|
||||
| `MCP_BEARER_TOKEN` | unset | Static bearer token for HTTP (admin identity). At least 32 characters. Generate with `openssl rand -hex 32`. |
|
||||
| `PUBLIC_BASE_URL` | unset | Public URL behind a reverse proxy. Same URL rules as `GITEA_URL`, and `https` unless the host is loopback. Required for OAuth. |
|
||||
| `ALLOWED_HOSTS` | empty | Extra `Host` header values accepted behind a proxy. The loopback names with the port and the host of `PUBLIC_BASE_URL` are always allowed. Other `Host` values are rejected (DNS-rebinding protection). A non-loopback `MCP_HOST` with neither `PUBLIC_BASE_URL` nor `ALLOWED_HOSTS` logs a startup warning on stderr, because remote clients then get 421. |
|
||||
| `FORWARDED_ALLOW_IPS` | `127.0.0.1` | Comma list of IP addresses or networks (or `*`) whose `X-Forwarded-For` and `X-Forwarded-Proto` headers are trusted. Set it to the address of the reverse proxy when it is not on the same host, for example a Docker network. Otherwise the per-IP rate limit sees the proxy address for every client. `*` trusts every peer and logs a warning: only use it when the port is reachable solely from the proxy, since clients could otherwise spoof their IP. |
|
||||
| `FORWARDED_ALLOW_IPS` | `127.0.0.1` | Comma list of IP addresses or networks (or `*`) whose `X-Forwarded-For` and `X-Forwarded-Proto` headers are trusted. Set it to the address of the reverse proxy when it is not on the same host, for example a Docker network. Otherwise the per-IP rate limit sees the proxy address for every client: a request that carries `X-Forwarded-For` from a peer outside this list is not honoured and logs one warning ("add its address to FORWARDED_ALLOW_IPS"). `*` trusts every peer and logs a warning: only use it when the port is reachable solely from the proxy, since clients could otherwise spoof their IP. |
|
||||
|
||||
## Per-user OAuth (HTTP only)
|
||||
|
||||
@@ -96,7 +96,7 @@ listed nor callable.
|
||||
| `SECRET_DETECTION_MODE` | `mask` | `off`, `mask` or `block`. See below. |
|
||||
| `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` or `CRITICAL`. Logs go to stderr. |
|
||||
| `EXPOSE_ERROR_DETAILS` | `false` | When true, the exception class name is added to internal and transport errors. Never a stack trace. Do not enable in production. |
|
||||
| `RATE_LIMIT_PER_MINUTE` | `120` | At least 1. Per client on the HTTP MCP endpoint, keyed by a hash of the Authorization header, else the client IP. Excess requests get 429. Only the first rejected request per client and minute is written to the audit log. Tracking is in memory and bounded (idle clients are forgotten). |
|
||||
| `RATE_LIMIT_PER_MINUTE` | `120` | At least 1. Per client on the HTTP MCP endpoint and on the unauthenticated OAuth endpoints (`/register`, `/authorize`, `/token`, `/revoke`, `/oauth/consent`, `/oauth/callback`), keyed by the client IP (an IPv6 client counts per /64) and, on the MCP endpoint, by a hash of the Authorization header. Excess requests get 429. Only the first rejected request per client and minute is written to the audit log. Tracking is in memory and bounded (idle clients are forgotten). |
|
||||
|
||||
Secret detection: `mask` replaces secret-like strings in results with a placeholder, `block`
|
||||
withholds a whole result that contains one (`secret_blocked`), `off` disables pattern matching.
|
||||
|
||||
+5
-4
@@ -97,7 +97,7 @@ Denial of service
|
||||
|---|---|---|
|
||||
| Large request bodies | SDK body limit on the MCP endpoint and SDK OAuth routes; upload cap checked before decoding | `server.py` (`max_request_body_size`), `gitea/encoding.py` (consent route: SEC-01) |
|
||||
| Large or hostile responses | 16 MiB text cap, `MAX_BINARY_BYTES`, 64 KiB error bodies, JSON depth 100, item and character limits | `gitea/executor.py` |
|
||||
| Request floods | Sliding window per IP and per token hash, bounded key sets, rate-limit events audited once per window | `rate_limit.py`, `transports.py` (gaps: SEC-08) |
|
||||
| Request floods | Sliding window per IP and per token hash, bounded key sets, rate-limit events audited once per window | `rate_limit.py`, `transports.py` (`ForwardedHeadersMiddleware`) |
|
||||
| Unbounded OAuth state | Caps on clients (500), pending sign-ins (1024), codes (1024), sessions per login (50); store pruning | `oauth/provider.py`, `oauth/store.py` (flush by floods: SEC-09) |
|
||||
|
||||
Elevation of privilege
|
||||
@@ -173,9 +173,10 @@ confirmation, `READ_ONLY`, `TOOLSETS`, `DISABLED_TOOLS` and narrow token scopes
|
||||
- DNS-rebinding protection on the MCP endpoint: `Host` must be a loopback name, the bind
|
||||
address, the `PUBLIC_BASE_URL` host or an `ALLOWED_HOSTS` entry (421 otherwise); a foreign
|
||||
`Origin` gets 403; POST must be `application/json`.
|
||||
- Rate limits: sliding window per client IP and per Authorization hash on the MCP path and the
|
||||
unauthenticated OAuth paths, before authentication. Client IPs come from `X-Forwarded-For`
|
||||
only for `FORWARDED_ALLOW_IPS`.
|
||||
- Rate limits: sliding window per client IP (IPv6 per /64) and per Authorization hash on the
|
||||
MCP path and the unauthenticated OAuth paths (`/register`, `/authorize`, `/token`, `/revoke`,
|
||||
consent, callback), before authentication. Client IPs come from `X-Forwarded-For` only for
|
||||
`FORWARDED_ALLOW_IPS`; a forwarded header from any other peer is ignored and logged once.
|
||||
- `PUBLIC_BASE_URL` must be https unless loopback. Gitea redirects are never followed.
|
||||
- stdio: stdout carries JSON-RPC only; all logging goes to stderr.
|
||||
|
||||
|
||||
Reference in new issue
Block a user