Traefik introduced an important entrypoint hardening option called underscoreHeadersStrategy in the v3.6.20 and v3.7.6 releases.
It controls how Traefik handles inbound HTTP headers whose names contain underscores before a request reaches routing and middleware processing.
The option supports three modes:
keepis the default and forwards underscore-containing headers unchanged.deletesilently removes every request header containing an underscore.rejectreturns400 Bad Requestwhen a request contains such a header.
At first glance, this may appear to be a minor normalization detail. In practice, it closes a dangerous header spoofing opportunity at the reverse-proxy boundary.
Why underscore headers are risky
Underscores are valid in HTTP header names. However, Go canonicalizes header names around dashes, not underscores. As a result, X-Auth-User and X_Auth_User remain separate header keys inside Go’s header map.
This matters when Traefik middleware manages a trusted identity header. For example, a ForwardAuth middleware may set or remove X-Auth-User before a request is forwarded to an application.
A malicious client could instead provide:
X_Auth_User: administratorThe middleware sees and controls the dash-based X-Auth-User header, while the underscore variant X_Auth_User can remain untouched.
Many backend environments normalize both forms to the same variable name. CGI, WSGI, PHP, and some NGINX or application-server configurations may expose both headers as HTTP_X_AUTH_USER. The backend can therefore read an attacker-controlled identity value even though Traefik correctly handled the dash-based header.
A related lesson from CVE-2026-33433
CVE-2026-33433 demonstrated how header canonicalization differences can lead to identity spoofing in proxy-based authentication flows.
The vulnerability affected BasicAuth and DigestAuth configurations using non-canonical headerField values, allowing an authenticated attacker to inject a competing header representation that the backend could prioritize.
The newly introduced field underscoreHeadersStrategy addresses a related defensive gap. Even where a middleware deletes or overwrites X-Auth-User, Go’s Header.Del("X-Auth-User") does not treat X_Auth_User as the same key. Without additional filtering, the underscore variant can survive and reach a backend that may treat the two forms as equivalent.
A related lesson from CVE-2024-45410
CVE-2024-45410 demonstrated how dangerous it can be when a backend application trusts headers added by a reverse proxy.
In affected Traefik versions, an HTTP/1.1 client could abuse the Connection header to mark selected forwarding headers, such as X-Forwarded-Host, as hop-by-hop. Traefik would then remove the header before the request reached the backend. The issue affected Traefik versions up to v2.11.8 and v3.1.2 and was fixed in v2.11.9 and v3.1.3.
The attack chain relies on two separate behaviors:
- A crafted
Connectionheader causes Traefik to remove the trusted dash-form header, such asX-Forwarded-Host. This was a CVE vulnerability but is nowadays fixed. - An attacker-controlled underscore variant, such as
X_Forwarded_Host, remains in the request. - Python-based environments including WSGI applications may normalize dashes and underscores to the same variable name.
- Once the trusted header is missing, the backend can read the attacker-controlled underscore variant instead.
The Shadow Corp CTF challenge provides a practical local demonstration. A Flask backend uses X-Forwarded-Host to determine whether a request should be treated as originating from a trusted source. As Flask and other Python-based servers commonly receive headers through WSGI-style environment variables, dash and underscore variants can be normalized to the same value. An attacker can therefore remove the trusted X-Forwarded-Host header through the Connection header issue and supply an attacker-controlled X_Forwarded_Host value that the backend interprets as the trusted header.
This is where underscoreHeadersStrategy provides valuable defense in depth. Setting the strategy to delete removes X_Forwarded_Host before it reaches the backend, preventing it from acting as a fallback or alias for the removed X-Forwarded-Host header. Using reject is even stricter and denies requests that contain underscore-based headers altogether.
Recommended configuration
For entrypoints that expose applications relying on trusted proxy headers, use delete unless underscore-based request headers are explicitly required:
entryPoints:
websecure:
address: ":443"
http:
underscoreHeadersStrategy: deleteUsing delete provides a practical compatibility-friendly safeguard. Suspicious alias headers are removed, while the request can continue normally. Use reject where strict request validation is preferred and clients should be notified immediately with a 400 Bad Request response.
The default keep mode preserves existing behavior, but it is not recommended for entry points that front CGI, WSGI, PHP, or other backends that normalize underscores and dashes identically. Treat proxy-managed identity and authorization headers as a trust boundary, and remove ambiguous header aliases before they reach the application.
Discussion