From c970d116bb49e6eef840fb5de304baec746c8711 Mon Sep 17 00:00:00 2001 From: Umputun Date: Thu, 20 Feb 2025 12:00:40 -0600 Subject: [PATCH] fix parsing headers with colons in the value, i.e. CSP --- app/proxy/handlers.go | 13 +++++---- app/proxy/handlers_test.go | 58 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/app/proxy/handlers.go b/app/proxy/handlers.go index 423d1d9..254ba09 100644 --- a/app/proxy/handlers.go +++ b/app/proxy/handlers.go @@ -18,20 +18,23 @@ import ( ) func headersHandler(addHeaders, dropHeaders []string) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if len(addHeaders) == 0 && len(dropHeaders) == 0 { next.ServeHTTP(w, r) return } + // add headers to response for _, h := range addHeaders { - elems := strings.Split(h, ":") - if len(elems) != 2 { - continue + // split on first colon only + if i := strings.Index(h, ":"); i >= 0 { + key := strings.TrimSpace(h[:i]) + value := strings.TrimSpace(h[i+1:]) + if key != "" { + w.Header().Set(key, value) + } } - w.Header().Set(strings.TrimSpace(elems[0]), strings.TrimSpace(elems[1])) } // drop headers from request diff --git a/app/proxy/handlers_test.go b/app/proxy/handlers_test.go index 0b48ff0..33db7dd 100644 --- a/app/proxy/handlers_test.go +++ b/app/proxy/handlers_test.go @@ -270,3 +270,61 @@ func TestHttp_basicAuthHandler(t *testing.T) { }) } } + +func TestHeaders_CSPParsing(t *testing.T) { + tbl := []struct { + name string + headers []string + expected map[string]string + }{ + { + name: "simple headers", + headers: []string{"X-Frame-Options:SAMEORIGIN", "X-XSS-Protection:1; mode=block"}, + expected: map[string]string{"X-Frame-Options": "SAMEORIGIN", "X-XSS-Protection": "1; mode=block"}, + }, + { + name: "CSP header with multiple directives", + headers: []string{"Content-Security-Policy:default-src 'self'; style-src 'self' 'unsafe-inline': something"}, + expected: map[string]string{"Content-Security-Policy": "default-src 'self'; style-src 'self' 'unsafe-inline': something"}, + }, + { + name: "CSP header with quotes and colons", + headers: []string{"Content-Security-Policy:script-src 'unsafe-inline' 'unsafe-eval' 'self' https://example.com:443"}, + expected: map[string]string{"Content-Security-Policy": "script-src 'unsafe-inline' 'unsafe-eval' 'self' https://example.com:443"}, + }, + { + name: "multiple colons in value", + headers: []string{"Custom-Header:value:with:colons"}, + expected: map[string]string{"Custom-Header": "value:with:colons"}, + }, + { + name: "empty value after colon", + headers: []string{"Empty-Header:"}, + expected: map[string]string{"Empty-Header": ""}, + }, + { + name: "malformed no colon", + headers: []string{"Bad-Header-No-Colon"}, + expected: map[string]string{}, + }, + } + + for _, tt := range tbl { + t.Run(tt.name, func(t *testing.T) { + wr := httptest.NewRecorder() + handler := headersHandler(tt.headers, nil)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + req := httptest.NewRequest("GET", "http://example.com", http.NoBody) + handler.ServeHTTP(wr, req) + + if len(tt.expected) == 0 { + // For malformed headers, check they weren't set + assert.Equal(t, 0, len(wr.Header())) + return + } + + for k, v := range tt.expected { + assert.Equal(t, v, wr.Header().Get(k), "Header %s value mismatch", k) + } + }) + } +}