GitHub Advisoryadvisory · Go

GHSA-pqg7-v6wh-3pfp TsDProxy: X-Forwarded-For header injection allows IP spoofing in proxied requests to backend services

HIGH Published 2026-07-14 Modified 2026-07-14 Official page ↗
Ecosystem
Go
Packages
github.com/almeidapaulopt/tsdproxy
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:N
Weaknesses
CWE-74
Reviewed
2026-07-14

Affected packages

• Go github.com/almeidapaulopt/tsdproxy: < 3.0.0-alpha.3 (no fix yet)

Description

Description The HTTP reverse proxy handler in tsdproxy does not strip the X-Forwarded-For (or X-Real-IP) header from incoming requests before calling r.SetXForwarded(). This allows an authenticated Tailscale user to inject arbitrary X-Forwarded-For values that are forwarded verbatim to backend services. ```go // internal/proxymanager/port.go -- Rewrite function Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(pconfig.GetFirstTarget()) r.Out.Host = r.In.Host // Strips tsdproxy identity headers (correct) r.Out.Header.Del(consts.HeaderID) r.Out.Header.Del(consts.HeaderRemoteUser) r.Out.Header.Del(consts.HeaderXForwardedUser) // ... other identity headers deleted ... // X-Forwarded-For is NOT deleted before SetXForwarded! // X-Real-IP is NOT deleted at all! r.SetXForwarded() // APPENDS client IP to attacker-controlled XFF list }, ``` Per Go's httputil.ProxyRequest.SetXForwarded() documentation: > If the inbound request has an existing X-Forwarded-For header, SetXForwarded appends the inbound request's remote address to the list. Result when attacker sends X-Forwarded-For: 127.0.0.1: • Backend receives: X-Forwarded-For: 127.0.0.1, • If backend reads first element as "original client", attacker appears as 127.0.0.1 X-Real-IP is not handled at all -- if the attacker sets X-Real-IP: 127.0.0.1, it is forwarded to the backend verbatim without any overriding or stripping. Many backend applications trust the first element of X-Forwarded-For (or X-Real-IP) for: • IP-based access control (admin panels restricted to 127.0.0.1) • Rate limiting tied to source IP • Audit logging • Geo-blocking or network-segment restrictions This is particularly impactful in tsdproxy's intended use case where the backend service is only accessible through tsdproxy -- making the proxy's header handling the sole enforcement point. CVSS CVSS v3.1: AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:N = 7.7 Severity High Affected Code / Files • `internal/proxymanager/port.go` -- newPortProxy Rewrite closure • Missing: r.Out.Header.Del("X-Forwarded-For") before r.SetXForwarded() • Missing: r.Out.Header.Del("X-Real-IP") unconditional strip Steps to Reproduce 1. Deploy tsdproxy with a backend service that restricts /admin to 127.0.0.1 via X-Forwarded-For (e.g., Nginx with real_ip_header X-Forwarded-For and real_ip_recursive on) 2. As an authenticated Tailscale user (non-admin), make a request through the proxy: ```bash curl -H "X-Forwarded-For: 127.0.0.1" \ https://.ts.net/admin ``` 3. Backend receives: X-Forwarded-For: 127.0.0.1, 4. Nginx real_ip_recursive resolves left-most non-trusted IP; if tailscale range is the only trusted range, 127.0.0.1 becomes the "real" IP, bypassing admin restriction. For the X-Real-IP vector: ```bash curl -H "X-Real-IP: 127.0.0.1" \ https://.ts.net/admin Backend receives X-Real-IP: 127.0.0.1 verbatim ``` PoC Script (if used) ```bash !/bin/bash Demonstrate XFF injection through tsdproxy PROXY_HOST="${1}" # e.g. myapp.my-tailnet.ts.net curl -v \ -H "X-Forwarded-For: 127.0.0.1" \ -H "X-Real-IP: 127.0.0.1" \ "https://${PROXY_HOST}/" Expected: backend sees XFF: 127.0.0.1, backend sees X-Real-IP: 127.0.0.1 (unmodified) ``` Impact An authenticated Tailscale user who should only have regular user access can: 1. Bypass IP-based admin restrictions on the backend application by spoofing X-Forwarded-For to 127.0.0.1 2. Appear as any arbitrary IP address in audit logs 3. Bypass rate limiting tied to source IP 4. Bypass geo-blocking or network-segment restrictions enforced by the backend This is especially impactful because tsdproxy is designed as the sole access point for backend services that are otherwise network-isolated -- making the proxy the only enforcement boundary. Fix: Add r.Out.Header.Del("X-Forwarded-For") and r.Out.Header.Del("X-Real-IP") in the Rewrite closure before calling r.SetXForwarded(). This ensures only the real Tailscale client IP appears in the XFF chain. Credits Reported by Vishal Shukla (@shukla304) using sechub.dev AI Agent Support If this disclosure work has been useful, sponsoring helps fund continued open-source security audits -- appreciated either way.

Related

Weakness · CWE (1)

  • CWE-74 Improper Neutralization of Special Elements in Output Used by a Downstream Component weakness

References