Skip to content

Commit a23da6e

Browse files
authored
Sanitize CORS headers (#85)
Add sanitation step for `Access-Control-Allow-Headers` when echoing back user supplied headers
1 parent 4c3aa40 commit a23da6e

File tree

3 files changed

+122
-1
lines changed

3 files changed

+122
-1
lines changed

proxy/proxymanager.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ func New(config *Config) *ProxyManager {
8484

8585
// allow whatever the client requested by default
8686
if headers := c.Request.Header.Get("Access-Control-Request-Headers"); headers != "" {
87-
c.Header("Access-Control-Allow-Headers", headers)
87+
sanitized := SanitizeAccessControlRequestHeaderValues(headers)
88+
c.Header("Access-Control-Allow-Headers", sanitized)
8889
} else {
8990
c.Header(
9091
"Access-Control-Allow-Headers",

proxy/sanitize_cors.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package proxy
2+
3+
import (
4+
"strings"
5+
)
6+
7+
func isTokenChar(r rune) bool {
8+
switch {
9+
case r >= 'a' && r <= 'z':
10+
case r >= 'A' && r <= 'Z':
11+
case r >= '0' && r <= '9':
12+
case strings.ContainsRune("!#$%&'*+-.^_`|~", r):
13+
default:
14+
return false
15+
}
16+
return true
17+
}
18+
19+
func SanitizeAccessControlRequestHeaderValues(headerValues string) string {
20+
parts := strings.Split(headerValues, ",")
21+
valid := make([]string, 0, len(parts))
22+
23+
for _, p := range parts {
24+
v := strings.TrimSpace(p)
25+
if v == "" {
26+
continue
27+
}
28+
29+
validPart := true
30+
for _, c := range v {
31+
if !isTokenChar(c) {
32+
validPart = false
33+
break
34+
}
35+
}
36+
37+
if validPart {
38+
valid = append(valid, v)
39+
}
40+
}
41+
42+
return strings.Join(valid, ", ")
43+
}

proxy/sanitize_cors_test.go

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package proxy
2+
3+
import "testing"
4+
5+
func TestSanitizeAccessControlRequestHeaderValues(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
input string
9+
expected string
10+
}{
11+
{
12+
name: "empty string",
13+
input: "",
14+
expected: "",
15+
},
16+
{
17+
name: "whitespace only",
18+
input: " ",
19+
expected: "",
20+
},
21+
{
22+
name: "single valid value",
23+
input: "content-type",
24+
expected: "content-type",
25+
},
26+
{
27+
name: "multiple valid values",
28+
input: "content-type, authorization, x-requested-with",
29+
expected: "content-type, authorization, x-requested-with",
30+
},
31+
{
32+
name: "values with extra spaces",
33+
input: " content-type , authorization ",
34+
expected: "content-type, authorization",
35+
},
36+
{
37+
name: "values with tabs",
38+
input: "content-type,\tauthorization",
39+
expected: "content-type, authorization",
40+
},
41+
{
42+
name: "values with invalid characters",
43+
input: "content-type, auth\n, x-requested-with\r",
44+
expected: "content-type, auth, x-requested-with",
45+
},
46+
{
47+
name: "empty values in list",
48+
input: "content-type,,authorization",
49+
expected: "content-type, authorization",
50+
},
51+
{
52+
name: "leading and trailing commas",
53+
input: ",content-type,authorization,",
54+
expected: "content-type, authorization",
55+
},
56+
{
57+
name: "mixed valid and invalid values",
58+
input: "content-type, \x00invalid, x-requested-with",
59+
expected: "content-type, x-requested-with",
60+
},
61+
{
62+
name: "mixed case values",
63+
input: "Content-Type, my-Valid-Header, Another-hEader",
64+
expected: "Content-Type, my-Valid-Header, Another-hEader",
65+
},
66+
}
67+
68+
for _, tt := range tests {
69+
t.Run(tt.name, func(t *testing.T) {
70+
got := SanitizeAccessControlRequestHeaderValues(tt.input)
71+
if got != tt.expected {
72+
t.Errorf("SanitizeAccessControlRequestHeaderValues(%q) = %q, want %q",
73+
tt.input, got, tt.expected)
74+
}
75+
})
76+
}
77+
}

0 commit comments

Comments
 (0)