{"id":"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\u003e 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, \u003creal-tailscale-client-ip\u003e\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://\u003cproxy-hostname\u003e.ts.net/admin\n```\n\n3. Backend receives: X-Forwarded-For: 127.0.0.1, \u003cyour-tailscale-ip\u003e\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://\u003cproxy-hostname\u003e.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, \u003ctailscale-ip\u003e\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.","aliases":["GO-2026-5982"],"modified":"2026-07-21T19:19:26.417432263Z","published":"2026-07-14T19:46:15Z","database_specific":{"github_reviewed_at":"2026-07-14T19:46:15Z","nvd_published_at":null,"cwe_ids":["CWE-74"],"severity":"HIGH","github_reviewed":true},"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"}],"affected":[{"package":{"name":"github.com/almeidapaulopt/tsdproxy","ecosystem":"Go","purl":"pkg:golang/github.com/almeidapaulopt/tsdproxy"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-pqg7-v6wh-3pfp/GHSA-pqg7-v6wh-3pfp.json","last_known_affected_version_range":"\u003c 3.0.0-alpha.3"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:N"}]}