Sanitize claim-derived identifier on auth-bypass headers (round 48)

This commit is contained in:
2026-08-13 15:14:57 +01:00
parent 44af200408
commit 7d6f09da87
2 changed files with 108 additions and 4 deletions
+16 -4
View File
@@ -99,11 +99,23 @@ func (t *TraefikOidc) applyBypassUserHeaders(req *http.Request, reason string) b
return false
}
req.Header.Set("X-Forwarded-User", userIdentifier)
if !t.minimalHeaders {
req.Header.Set("X-Auth-Request-User", userIdentifier)
// Sanitize the claim-derived identifier before it is injected as a
// header, matching forwardAuthorized (which uses safeIdentifier). The
// value originates from an IdP-controlled claim and can contain CRLF
// or delimiter characters that would otherwise inject or confuse
// downstream header parsing. On failure, drop the header but still
// honor the bypass (the identity headers are decoration here).
safeIdentifier, ok := sanitizeHeaderClaimValue(userIdentifier, t.headerClaimMaxLen())
if !ok {
t.logger.Debugf("%s bypass: dropping unsafe user-identifier header: %s", reason, headerClaimValueReason(userIdentifier, t.headerClaimMaxLen()))
return true
}
t.logger.Debugf("%s bypass: forwarded user %s from session", reason, userIdentifier)
req.Header.Set("X-Forwarded-User", safeIdentifier)
if !t.minimalHeaders {
req.Header.Set("X-Auth-Request-User", safeIdentifier)
}
t.logger.Debugf("%s bypass: forwarded user %s from session", reason, safeIdentifier)
return true
}
+92
View File
@@ -0,0 +1,92 @@
package traefikoidc
import (
"net/http"
"net/http/httptest"
"testing"
)
// TestApplyBypassUserHeaders_SanitizesIdentifier is a regression test for the
// auth-bypass header path (applyBypassUserHeaders). The claim-derived user
// identifier is injected into X-Forwarded-User / X-Auth-Request-User without
// sanitization, unlike forwardAuthorized which uses sanitizeHeaderClaimValue.
// An IdP-controlled identifier containing CRLF (or a delimiter) would
// otherwise inject or confuse downstream header parsing. Now the bypass path
// sanitizes the identifier and drops the header on failure.
func TestApplyBypassUserHeaders_SanitizesIdentifier(t *testing.T) {
sm := createTestSessionManager(t)
base := httptest.NewRequest(http.MethodGet, "/protected", nil)
baseRec := httptest.NewRecorder()
// Build an authenticated session whose user identifier contains CRLF.
session, err := sm.GetSession(base)
if err != nil {
t.Fatalf("GetSession: %v", err)
}
if err := session.SetAuthenticated(true); err != nil {
t.Fatalf("SetAuthenticated: %v", err)
}
session.SetUserIdentifier("user\nX-Injected: yes")
if err := session.Save(base, baseRec); err != nil {
t.Fatalf("Save: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
for _, c := range baseRec.Result().Cookies() {
req.AddCookie(c)
}
oidc := &TraefikOidc{
sessionManager: sm,
logger: NewLogger("debug"),
}
if ok := oidc.applyBypassUserHeaders(req, "test"); !ok {
t.Fatalf("applyBypassUserHeaders returned false; bypass should be honored")
}
if v := req.Header.Get("X-Forwarded-User"); v != "" {
t.Fatalf("X-Forwarded-User = %q, want empty (unsafe identifier dropped)", v)
}
if v := req.Header.Get("X-Auth-Request-User"); v != "" {
t.Fatalf("X-Auth-Request-User = %q, want empty (unsafe identifier dropped)", v)
}
}
// TestApplyBypassUserHeaders_KeepsCleanIdentifier ensures a normal identifier
// still flows through, and is trimmed/sanitized.
func TestApplyBypassUserHeaders_KeepsCleanIdentifier(t *testing.T) {
sm := createTestSessionManager(t)
base := httptest.NewRequest(http.MethodGet, "/protected", nil)
baseRec := httptest.NewRecorder()
session, err := sm.GetSession(base)
if err != nil {
t.Fatalf("GetSession: %v", err)
}
if err := session.SetAuthenticated(true); err != nil {
t.Fatalf("SetAuthenticated: %v", err)
}
session.SetUserIdentifier(" [email protected] ")
if err := session.Save(base, baseRec); err != nil {
t.Fatalf("Save: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
for _, c := range baseRec.Result().Cookies() {
req.AddCookie(c)
}
oidc := &TraefikOidc{
sessionManager: sm,
logger: NewLogger("debug"),
}
if ok := oidc.applyBypassUserHeaders(req, "test"); !ok {
t.Fatalf("applyBypassUserHeaders returned false; bypass should be honored")
}
if v := req.Header.Get("X-Forwarded-User"); v != "[email protected]" {
t.Fatalf("X-Forwarded-User = %q, want %q (trimmed)", v, "[email protected]")
}
}