GHSA-PQG7-V6WH-3PFP

Vulnerability from github – Published: 2026-07-14 19:46 – Updated: 2026-07-14 19:46
VLAI
Summary
TsDProxy: X-Forwarded-For header injection allows IP spoofing in proxied requests to backend services
Details

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.

// 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:
curl -H "X-Forwarded-For: 127.0.0.1" \
     https://<proxy-hostname>.ts.net/admin
  1. Backend receives: X-Forwarded-For: 127.0.0.1,
  2. 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:

curl -H "X-Real-IP: 127.0.0.1" \
     https://<proxy-hostname>.ts.net/admin
# Backend receives X-Real-IP: 127.0.0.1 verbatim

PoC Script (if used)

#!/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, <tailscale-ip>
#           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.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c 3.0.0-alpha.3"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/almeidapaulopt/tsdproxy"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-74"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-14T19:46:15Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "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\u0027s httputil.ProxyRequest.SetXForwarded() documentation:\n\u003e If the inbound request has an existing X-Forwarded-For header, SetXForwarded appends the inbound request\u0027s 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\u0027s intended use case where the backend service is only accessible through tsdproxy -- making the proxy\u0027s 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.",
  "id": "GHSA-pqg7-v6wh-3pfp",
  "modified": "2026-07-14T19:46:15Z",
  "published": "2026-07-14T19:46:15Z",
  "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "TsDProxy: X-Forwarded-For header injection allows IP spoofing in proxied requests to backend services"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…