mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-11-03 07:44:13 +00:00
fix(security): drop client values for templated headers
A templated header skipped its Set when the render failed, came out empty or failed sanitising. The client's own value of that header then reached the backend. A client could forge a header such as X-Tenant by using a token that lacks the claim. The middleware now removes the inbound value first. Identity headers and Authorization are left alone.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func ownedHeaderOIDC(t *testing.T, names ...string) *TraefikOidc {
|
||||
t.Helper()
|
||||
m := map[string]*template.Template{}
|
||||
for _, n := range names {
|
||||
m[n] = template.Must(template.New(n).Option("missingkey=zero").Parse("{{.Claims.tenant}}"))
|
||||
}
|
||||
return &TraefikOidc{logger: NewLogger("error"), headerTemplates: m}
|
||||
}
|
||||
|
||||
// A templated header the middleware owns must never carry a client value:
|
||||
// an optional claim that renders empty must leave the header absent.
|
||||
func TestTemplatedHeaderDropsClientSpoof(t *testing.T) {
|
||||
o := ownedHeaderOIDC(t, "X-Tenant")
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("X-Tenant", "admin-corp")
|
||||
o.applyHeaderTemplates(req, &principal{Claims: map[string]interface{}{"sub": "u"}})
|
||||
if got := req.Header.Get("X-Tenant"); got != "" {
|
||||
t.Fatalf("client-supplied X-Tenant reached the backend: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplatedHeaderRenderedValueWins(t *testing.T) {
|
||||
o := ownedHeaderOIDC(t, "X-Tenant")
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("X-Tenant", "evil")
|
||||
o.applyHeaderTemplates(req, &principal{Claims: map[string]interface{}{"tenant": "acme"}})
|
||||
if got := req.Header.Get("X-Tenant"); got != "acme" {
|
||||
t.Fatalf("X-Tenant = %q, want acme", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Identity headers are injected before templates run; an empty render must
|
||||
// not clobber them, and Authorization stays as received.
|
||||
func TestTemplatedHeaderKeepsIdentityAndAuthorization(t *testing.T) {
|
||||
o := ownedHeaderOIDC(t, "X-Forwarded-User", "Authorization")
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("X-Forwarded-User", "alice")
|
||||
req.Header.Set("Authorization", "Bearer abc")
|
||||
o.applyHeaderTemplates(req, &principal{Claims: map[string]interface{}{"sub": "u"}})
|
||||
if got := req.Header.Get("X-Forwarded-User"); got != "alice" {
|
||||
t.Fatalf("identity header clobbered: %q", got)
|
||||
}
|
||||
if got := req.Header.Get("Authorization"); got != "Bearer abc" {
|
||||
t.Fatalf("Authorization changed: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -1341,6 +1341,23 @@ func joinBoundedClaimHeader(values []string) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// dropInboundTemplated removes a client-supplied value for a header the
|
||||
// middleware owns, so a skipped render leaves it absent instead of forwarding
|
||||
// the client's. Identity headers (already stripped and re-injected) and
|
||||
// Authorization (the inbound bearer credential) are left alone.
|
||||
func dropInboundTemplated(req *http.Request, name string) {
|
||||
canon := http.CanonicalHeaderKey(name)
|
||||
if canon == "Authorization" {
|
||||
return
|
||||
}
|
||||
for _, h := range identityHeaders {
|
||||
if canon == http.CanonicalHeaderKey(h) {
|
||||
return
|
||||
}
|
||||
}
|
||||
req.Header.Del(canon)
|
||||
}
|
||||
|
||||
// applyHeaderTemplates renders the configured header templates for p onto req.
|
||||
func (t *TraefikOidc) applyHeaderTemplates(req *http.Request, p *principal) {
|
||||
if len(t.headerTemplates) == 0 {
|
||||
@@ -1359,6 +1376,7 @@ func (t *TraefikOidc) applyHeaderTemplates(req *http.Request, p *principal) {
|
||||
}
|
||||
|
||||
for headerName, tmpl := range t.headerTemplates {
|
||||
dropInboundTemplated(req, headerName)
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, templateData); err != nil {
|
||||
t.logger.Errorf("Failed to execute template for header %s: %v", headerName, err)
|
||||
|
||||
Reference in New Issue
Block a user