high · 8.5Jul 14, 2026

TsDProxy X-Forwarded-For Header Injection Allows IP Spoofing

Rohit Hatagale
AI Security Researcher, SecureLayer7

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

Packagegithub.com/almeidapaulopt/tsdproxy
Ecosystemgo
Affected< 3.0.0-alpha.3

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.

bash
# 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).

References: [1][2][3]

Related research