mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-11-08 08:34:07 +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()
|
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.
|
// applyHeaderTemplates renders the configured header templates for p onto req.
|
||||||
func (t *TraefikOidc) applyHeaderTemplates(req *http.Request, p *principal) {
|
func (t *TraefikOidc) applyHeaderTemplates(req *http.Request, p *principal) {
|
||||||
if len(t.headerTemplates) == 0 {
|
if len(t.headerTemplates) == 0 {
|
||||||
@@ -1359,6 +1376,7 @@ func (t *TraefikOidc) applyHeaderTemplates(req *http.Request, p *principal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for headerName, tmpl := range t.headerTemplates {
|
for headerName, tmpl := range t.headerTemplates {
|
||||||
|
dropInboundTemplated(req, headerName)
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := tmpl.Execute(&buf, templateData); err != nil {
|
if err := tmpl.Execute(&buf, templateData); err != nil {
|
||||||
t.logger.Errorf("Failed to execute template for header %s: %v", headerName, err)
|
t.logger.Errorf("Failed to execute template for header %s: %v", headerName, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user