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…

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