TsDProxy X-Forwarded-For Header Injection Allows IP Spoofing
tsdproxy's HTTP reverse proxy forwards attacker-supplied X-Forwarded-For and X-Real-IP headers verbatim to backend services, letting any authenticated Tailscale user spoof their apparent source IP and
The problem
The `Rewrite` closure in `internal/proxymanager/port.go` correctly strips tsdproxy identity headers before forwarding requests, but never deletes `X-Forwarded-For` or `X-Real-IP` from the inbound request.
When `r.SetXForwarded()` is called, Go's `httputil.ProxyRequest` appends the real Tailscale client IP to whatever `X-Forwarded-For` value the attacker already injected. The backend receives the attacker-controlled value at the head of the list. `X-Real-IP` is passed through entirely unchanged.
Backend services that trust the first element of `X-Forwarded-For`, or that trust `X-Real-IP` without overriding it, will operate on the attacker-supplied IP. This breaks IP-based admin restrictions, rate limiting, audit logging, and geo-blocking. The impact is heightened because tsdproxy is typically the sole access path to otherwise network-isolated backends.
Proof of concept
A working proof-of-concept for this issue in github.com/almeidapaulopt/tsdproxy, with the exact payload below.
# Vector 1: X-Forwarded-For injection
# Backend receives: X-Forwarded-For: 127.0.0.1, <real-tailscale-ip>
curl -H "X-Forwarded-For: 127.0.0.1" \
https://<proxy-hostname>.ts.net/admin
# Vector 2: X-Real-IP injection (forwarded verbatim, no stripping)
curl -H "X-Real-IP: 127.0.0.1" \
https://<proxy-hostname>.ts.net/admin
# Combined PoC
curl -v \
-H "X-Forwarded-For: 127.0.0.1" \
-H "X-Real-IP: 127.0.0.1" \
"https://<proxy-hostname>.ts.net/"Root cause is missing header sanitization before `r.SetXForwarded()` in the `Rewrite` closure. Go's `httputil.ProxyRequest.SetXForwarded()` appends the connecting client IP to any pre-existing `X-Forwarded-For` value rather than replacing it, so an attacker who sets `X-Forwarded-For: 127.0.0.1` causes the backend to receive `127.0.0.1, <tailscale-ip>`.
Backends reading the leftmost entry treat `127.0.0.1` as the originating client.
The patch commit `e8200b7947719e5e7fbbbdb9c34f459a4c285e77` adds two explicit deletes in the `Rewrite` closure before the `r.SetXForwarded()` call: `r.Out.Header.Del("X-Forwarded-For")` and `r.Out.Header.Del("X-Real-IP")`. This ensures `SetXForwarded()` starts from a clean slate and writes only the verified Tailscale client IP.
CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component (Injection).
The fix
Upgrade to tsdproxy v3.0.0-alpha.3 or later. The fix is in commit `e8200b7947719e5e7fbbbdb9c34f459a4c285e77`: add `r.Out.Header.Del("X-Forwarded-For")` and `r.Out.Header.Del("X-Real-IP")` inside the `Rewrite` closure in `internal/proxymanager/port.go`, immediately before the call to `r.SetXForwarded()`.
No configuration change is required once upgraded.
Reported by Vishal Shukla (@shukla304).
Related research
- critical · 9TSDProxy: Internal Auth Token Leaked to Backend Services Enables Management API Takeover
- high · 7.5CVE-2026-54629CVE-2026-54629: Anyquery Local File Read via Unrestricted SQLite Virtual Table Modules
- highCVE-2026-54448CVE-2026-54448: Trivy Helm Chart Tar Bomb OOM via Unbounded io.ReadAll
- highEch0: Accept-Language `_` Separator Bypass Causes ~70x CPU Amplification DoS