Files
kaya/packages/kaya-forwarded

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

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.