Refactor forwarded header handling into opt-in kaya-forwarded package with trusted CIDRs
CI / Build Pip package (push) Successful in 3m59s
CI / Build Pip package (push) Successful in 3m59s
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# kaya-forwarded
|
||||
|
||||
Trusted-proxy forwarded header handling for the Kaya web framework.
|
||||
|
||||
Without this package, Kaya exposes the raw socket peer address as
|
||||
`ctx.client` / `ws.client` and ignores `Forwarded` / `X-Forwarded-*` headers
|
||||
entirely (they are client-controllable and trivially spoofable when the app is
|
||||
directly exposed).
|
||||
|
||||
`ForwardedHeadersMixin` opts the application into honoring those headers, but
|
||||
only when the direct socket peer is a trusted proxy, identified by a list of
|
||||
trusted CIDRs/IPs.
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from kaya.core import KayaApp, HttpContext
|
||||
from kaya.forwarded import ForwardedHeadersMixin
|
||||
|
||||
app = KayaApp(mixins=[
|
||||
ForwardedHeadersMixin(trusted_proxies=['127.0.0.1', '::1', '10.0.0.0/8'])
|
||||
])
|
||||
|
||||
@app.GET('/whoami')
|
||||
async def whoami(ctx: HttpContext):
|
||||
host, port = ctx.client
|
||||
await ctx.send_str(200, f'{host}:{port}')
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
When a request arrives:
|
||||
|
||||
1. If the socket peer IP does not belong to any trusted CIDR (or there is no
|
||||
peer address), the mixin leaves the context untouched — `client` remains
|
||||
the socket peer and all proxy headers are ignored.
|
||||
2. Otherwise the client address is resolved from the headers, in order:
|
||||
- `Forwarded` (RFC 7239): the `for=` entries are walked **from right to
|
||||
left**, skipping entries that are themselves trusted proxies (and
|
||||
`unknown`); the first untrusted entry is the client. This defeats
|
||||
spoofing when the edge proxy *appends* to the header (e.g. nginx with
|
||||
`$proxy_add_x_forwarded_for`), because attacker-supplied leftmost entries
|
||||
are never selected. A `:port` in the selected `for=` value also
|
||||
populates the port.
|
||||
- `X-Forwarded-For`: same right-to-left trusted-proxy walk; the port comes
|
||||
from `X-Forwarded-Port` when present and valid.
|
||||
- `X-Forwarded-Host`: first entry; port from `X-Forwarded-Port` as above.
|
||||
3. If none of the headers are present or usable, the socket peer is kept.
|
||||
|
||||
If every entry in the chain is a trusted proxy, the leftmost entry is used
|
||||
(the whole chain is trusted, so the leftmost is the original client).
|
||||
|
||||
The resolved address is exposed by wrapping the request context /
|
||||
websocket (the same pattern as `kaya-session`), so both ASGI and RSGI keep
|
||||
working and `ctx.session` from other mixins is preserved.
|
||||
|
||||
## Note
|
||||
|
||||
Even with this mixin, the edge proxy should still strip or overwrite inbound
|
||||
`Forwarded` / `X-Forwarded-*` headers from clients — the mixin protects the
|
||||
application, the proxy protects the chain.
|
||||
Reference in New Issue
Block a user