GHSA-pqg7-v6wh-3pfp TsDProxy: X-Forwarded-For header injection allows IP spoofing in proxied requests to backend services
DescriptionThe 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.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, <real-tailscale-client-ip>If backend reads first element as "original client", attacker appears as 127.0.0.1X-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 IPAudit loggingGeo-blocking or network-segment restrictionsThis 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.CVSSCVSS v3.1: AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:N = 7.7SeverityHighAffected Code / Files`internal/proxymanager/port.go` -- newPortProxy Rewrite closureMissing: r.Out.Header.Del("X-Forwarded-For") before r.SetXForwarded()Missing: r.Out.Header.Del("X-Real-IP") unconditional stripSteps to ReproduceDeploy 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)As an authenticated Tailscale user (non-admin), make a request through the proxy:Backend receives: X-Forwarded-For: 127.0.0.1, <your-tailscale-ip>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:PoC Script (if used)ImpactAn authenticated Tailscale user who should only have regular user access can:Bypass IP-based admin restrictions on the backend application by spoofing X-Forwarded-For to 127.0.0.1Appear as any arbitrary IP address in audit logsBypass rate limiting tied to source IPBypass geo-blocking or network-segment restrictions enforced by the backendThis 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.CreditsReported by Vishal Shukla (@shukla304) using sechub.dev AI AgentSupportIf this disclosure work has been useful, [sponsoring](https://github.com/sponsors/therawdev) helps fund continued open-source security audits -- appreciated either way.
CVSS v3.1 score: 8.5