All Posts programming Fixing Header Sanitization in Traefik’s PassTLSClientCert Middleware: A Deep Dive into PR #12875

Fixing Header Sanitization in Traefik’s PassTLSClientCert Middleware: A Deep Dive into PR #12875

· 1018 words · 5 minute read
Go in production: interesting moments ▹

Traefik is one of the most popular open-source reverse proxies and load balancers, loved by developers for its simplicity, dynamic configuration, and seamless integration with Docker, Kubernetes, and other orchestrators. It sits between your clients and backend services, handling routing, TLS termination, and much more.

Deploying Your Container with HTTPS Using Traefik as a Reverse Proxy source: Deploying Your Container with HTTPS Using Traefik as a Reverse Proxy

One powerful feature is mutual TLS (mTLS) authentication, where both the client and the server prove their identity using certificates. After Traefik verifies a client certificate, you often want to forward details about that certificate to your backend application (for logging, authorization, or auditing). That’s exactly what the PassTLSClientCert HTTP middleware does.

But there was a subtle security bug in how this middleware handled headers — and Pull Request #12875 fixes it cleanly. Let’s walk through what the problem was, why it mattered, how the PR solves it, and what the code changes look like. By the end, you’ll see exactly how the goals of the PR were achieved.

What Does PassTLSClientCert Actually Do? 🔗

When you enable mTLS on a Traefik router, the client presents a certificate during the TLS handshake. Traefik extracts information from req.TLS.PeerCertificates and can add two special headers to the request before it reaches your backend:

  • X-Forwarded-Tls-Client-Cert: Contains the full PEM-encoded client certificate (if you set pem: true).
  • X-Forwarded-Tls-Client-Cert-Info: Contains structured details (subject, issuer, validity dates, SANs, etc.) in an escaped string (if you configure the info section).

Here’s a typical configuration example:

http:
  middlewares:
    pass-client-cert:
      passTLSClientCert:
        pem: true
        info:
          subject: true
          issuer: true
          notBefore: true
          notAfter: true

Your backend service can then read these headers and trust that they came from a verified TLS connection.

how mutual authentication works source: what is mtls tls mutual authentication

The Problem: Unsanitized Headers Could Leak Through 🔗

Before PR #12875 , the middleware had a classic header injection / data leakage vulnerability.

Imagine this scenario:

  1. A malicious client (or an upstream proxy) sends a request that already includes X-Forwarded-Tls-Client-Cert or X-Forwarded-Tls-Client-Cert-Info headers with fake or harmful values.
  2. Traefik performs mTLS, extracts the real certificate data, and calls req.Header.Set(...) to add the correct values.
  3. But it never removed the original headers.

In Go’s http.Header, calling Set replaces the value for that specific key, but if the middleware logic was structured in a way that the original value persisted (or was appended in certain edge cases), the backend could receive unsanitized data. Worse, an attacker could craft headers that looked legitimate or contained malicious payloads.

This broke the sanitation guarantee that the middleware was supposed to provide: “Only data extracted from the actual TLS connection should ever reach the backend”.

traditional TLS vs mTLS connections source: what is mtls tls mutual authentication

The Fix: Explicit Header Deletion Before Setting Sanitized Values 🔗

PR #12875 , authored by Mathis (@LBF38) , makes one focused, high-impact change in the middleware code.

Key files changed:

  • pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go (the core logic)
  • pkg/middlewares/passtlsclientcert/pass_tls_client_cert_test.go (new tests)

Here’s the before vs. after in the critical part of ServeHTTP:

// BEFORE (simplified)
if p.pem {
    if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
        req.Header.Set(xForwardedTLSClientCert, getCertificates(...))
    }
}
req.Header.Set(xForwardedTLSClientCertInfo, ...)

// AFTER (the fix)
ctx := middlewares.GetLoggerCtx(...)
logger := log.FromContext(ctx)

// 🔥 Explicitly delete any incoming (potentially malicious) headers
req.Header.Del(xForwardedTLSClientCert)

if p.pem {
    if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
        req.Header.Set(xForwardedTLSClientCert, getCertificates(ctx, req.TLS.PeerCertificates))
    }
}

req.Header.Del(xForwardedTLSClientCertInfo)

if p.info != nil {
    if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
        headerContent := p.getCertInfo(ctx, req.TLS.PeerCertificates)
        req.Header.Set(xForwardedTLSClientCertInfo, headerContent)
    }
}

Why this works:

  • req.Header.Del(key) completely removes the header (and all values for that key) from the request.
  • Only after deletion does the middleware set the new, trusted value derived from req.TLS.PeerCertificates.
  • If no valid peer certificates exist, the headers are simply not set — no stale data remains.

This is a textbook example of proper header sanitization in proxy middleware.

New Tests Prove the Fix 🔗

The PR also added clear test cases that simulate an attacker sending fake headers:

req.Header.Set(xForwardedTLSClientCert, "Unsanitized HEADER")  // malicious input
// ... set real TLS certs ...
// After middleware runs:
assert.Equal(t, sanitizedCertValue, req.Header.Get(xForwardedTLSClientCert))
// Original "Unsanitized HEADER" is gone!

These tests ensure the behavior is rock-solid and will catch regressions in future releases.

Why This PR Matters (The “Why” Behind the Commits) 🔗

  • Security: Prevents header injection attacks and ensures downstream services can always trust the certificate data.
  • Consistency: Matches how other Traefik middlewares (and industry best practices like those in Nginx or Envoy) handle forwarded headers.
  • Minimal & Safe: No new features, no breaking changes — just a surgical fix that improves reliability without touching configuration, TLS handshake, or routing logic.

The commits were deliberately small and focused:

  1. Update the middleware logic with explicit Del() calls.
  2. Add comprehensive tests.
  3. (Implicitly) Update any internal documentation or changelog references.

Goals of the Pull Request — Clearly Achieved ✅ 🔗

By the end of the PR:

  • Goal 1: Stop unsanitized headers from reaching backends → Achieved with req.Header.Del().
  • Goal 2: Ensure only real TLS peer certificate data is forwarded → Achieved (still uses req.TLS.PeerCertificates).
  • Goal 3: Maintain backward compatibility and zero impact on existing users → Achieved (no config changes required).
  • Goal 4: Prove the fix works → Achieved with new unit tests.

This tiny but critical improvement makes Traefik even more trustworthy when you’re using mTLS in production.

Wrapping Up 🔗

Header sanitization might sound like a small detail, but in a reverse proxy that handles millions of requests, it’s the difference between “secure by default” and “vulnerable to clever attackers”. PR #12875 is a perfect example of proactive security engineering in open-source software.

If you use PassTLSClientCert today, update to the latest Traefik version that includes this fix. Your backends will thank you — and your security team will too.

Thanks to contributor Mathis (@LBF38) for the clean fix — this is exactly why open-source communities rock.

I hope you enjoyed reading this post as much as I enjoyed writing it. If you know a person who can benefit from this information, send them a link of this post. If you want to get notified about new posts, follow me on YouTube , Twitter (x) , LinkedIn , and GitHub .

Go in production: interesting moments ▹