Skip to content

TSDProxy: Internal proxy auth token forwarded to backend services enables management API escalation

Critical severity GitHub Reviewed Published Jun 3, 2026 in almeidapaulopt/tsdproxy • Updated Jul 10, 2026

Package

gomod github.com/almeidapaulopt/tsdproxy (Go)

Affected versions

< 1.4.4-0.20260603142855-434819b4421e

Patched versions

1.4.4-0.20260603142855-434819b4421e

Description

Description

A vulnerability was discovered in TSDProxy where it forwards its internal per-process authentication token to all proxied backend services. When identityHeaders is enabled (the default), tsdproxy injects x-tsdproxy-auth-token into every upstream HTTP request alongside user identity headers. This token is the same secret used by the management HTTP server to trust forwarded Tailscale identity claims. A backend that receives this token can replay it from localhost to the management port with an arbitrary x-tsdproxy-id value, bypassing Tailscale authentication entirely.

The token is forwarded unconditionally: ProviderUserMiddleware always calls WhoisNewContext regardless of whether the user is authenticated. In the ReverseProxy.Rewrite function, WhoisFromContext returns ok=true even for zero-value Whois{} (unauthenticated or Funnel requests). The HeaderAuthToken is set for every request when identityHeaders=true.

The attack requires the backend to reach 127.0.0.1:8080. This holds in: (1) non-Docker deployments where tsdproxy and a backend run on the same host, (2) Docker host-network-mode containers, (3) containers sharing tsdproxy's network namespace.

Affected files

  • internal/proxymanager/port.go:123-132
  • internal/core/admin.go:160-182
// port.go: auth token forwarded regardless of user authentication state
if identityHeaders {
    if user, ok := model.WhoisFromContext(r.In.Context()); ok {
        // ok=true even for empty Whois{} stored by ProviderUserMiddleware
        r.Out.Header.Set(consts.HeaderAuthToken, core.ProxyAuthToken()) // token sent to backend
    }
}

// admin.go: management port trusts x-tsdproxy-id from localhost when token is valid
func ResolveWhois(r *http.Request) model.Whois {
    if IsLocalhost(r.RemoteAddr) {
        return model.Whois{
            ID: r.Header.Get(consts.HeaderID), // attacker-controlled after stealing token
        }
    }
    return model.Whois{}
}

Steps to reproduce

  1. Deploy tsdproxy on a host (non-Docker) with a backend at http://localhost:3000.
  2. Make a request through the Tailscale proxy. The backend receives x-tsdproxy-auth-token in the request headers.
  3. From the host, replay the token to the management API:
# Capture token from backend headers (e.g., via a header-reflection endpoint)
TOKEN=$(curl -s http://localhost:3000/debug/headers | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['headers'].get('X-Tsdproxy-Auth-Token',''))")

# Replay from localhost to gain admin access
curl -H "x-tsdproxy-auth-token: $TOKEN" \
     -H "x-tsdproxy-id: attacker" \
     http://127.0.0.1:8080/api/v1/proxies
# Returns full proxy list with admin access

Fix

Remove HeaderAuthToken from the outgoing backend request, and guard identity-header injection on user.ID != "":

// port.go: only inject headers for actually authenticated users
if identityHeaders {
    if user, ok := model.WhoisFromContext(r.In.Context()); ok && user.ID != "" {
        r.Out.Header.Set(consts.HeaderID, user.ID)
        // HeaderAuthToken should NOT be forwarded to backends
    }
}

Impact

An attacker with code execution in any backend proxied by tsdproxy (on the same host) gains full management API control: restart or pause all proxied services (DoS), enumerate all proxy configurations and backend network topology, and trigger webhook deliveries (SSRF via configured webhook URLs).

Credits

Reported by Vishal Shukla (@shukla304 / @therawdev).

Sponsorship

This audit is from an AI-assisted research agent at sechub.dev. Running it on OSS projects is free for maintainers.

References

@almeidapaulopt almeidapaulopt published to almeidapaulopt/tsdproxy Jun 3, 2026
Published to the GitHub Advisory Database Jul 10, 2026
Reviewed Jul 10, 2026
Last updated Jul 10, 2026

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H

EPSS score

Weaknesses

Exposure of Sensitive Information to an Unauthorized Actor

The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. Learn more on MITRE.

Improper Authentication

When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct. Learn more on MITRE.

CVE ID

No known CVE

GHSA ID

GHSA-g936-7jqj-mwv8

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.