Skip to content

Commit 7792dba

Browse files
authored
Add HTTP header validation to prevent injection (#2411)
Add ValidateHTTPHeaderName and ValidateHTTPHeaderValue functions to the validation package to prevent CRLF injection and other header-based attacks. These functions use golang.org/x/net/http/httpguts for RFC 7230 compliant validation, matching Go's own HTTP/2 implementation. The validation checks for: - CRLF injection attempts (\r\n) - Control characters (null bytes, etc.) - RFC 7230 token compliance for header names - Length limits (256 bytes for names, 8KB for values)
1 parent 6a71fdb commit 7792dba

File tree

2 files changed

+126
-0
lines changed

2 files changed

+126
-0
lines changed

pkg/validation/validation.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"regexp"
77
"strings"
8+
9+
"golang.org/x/net/http/httpguts"
810
)
911

1012
var validGroupNameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-\s]+$`)
@@ -39,3 +41,43 @@ func ValidateGroupName(name string) error {
3941

4042
return nil
4143
}
44+
45+
// ValidateHTTPHeaderName validates that a string is a valid HTTP header name per RFC 7230.
46+
// It checks for CRLF injection, control characters, and ensures RFC token compliance.
47+
func ValidateHTTPHeaderName(name string) error {
48+
if name == "" {
49+
return fmt.Errorf("header name cannot be empty")
50+
}
51+
52+
// Length limit to prevent DoS
53+
if len(name) > 256 {
54+
return fmt.Errorf("header name exceeds maximum length of 256 bytes")
55+
}
56+
57+
// Use httpguts validation (same as Go's HTTP/2 implementation)
58+
if !httpguts.ValidHeaderFieldName(name) {
59+
return fmt.Errorf("invalid HTTP header name: contains invalid characters")
60+
}
61+
62+
return nil
63+
}
64+
65+
// ValidateHTTPHeaderValue validates that a string is a valid HTTP header value per RFC 7230.
66+
// It checks for CRLF injection and control characters.
67+
func ValidateHTTPHeaderValue(value string) error {
68+
if value == "" {
69+
return fmt.Errorf("header value cannot be empty")
70+
}
71+
72+
// Length limit to prevent DoS (common HTTP server limit)
73+
if len(value) > 8192 {
74+
return fmt.Errorf("header value exceeds maximum length of 8192 bytes")
75+
}
76+
77+
// Use httpguts validation
78+
if !httpguts.ValidHeaderFieldValue(value) {
79+
return fmt.Errorf("invalid HTTP header value: contains control characters")
80+
}
81+
82+
return nil
83+
}

pkg/validation/validation_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package validation_test
22

33
import (
4+
"strings"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
@@ -57,3 +58,86 @@ func TestValidateGroupName(t *testing.T) {
5758
})
5859
}
5960
}
61+
62+
func TestValidateHTTPHeaderName(t *testing.T) {
63+
t.Parallel()
64+
65+
tests := []struct {
66+
name string
67+
input string
68+
expectErr bool
69+
}{
70+
// Valid cases
71+
{"valid simple", "X-API-Key", false},
72+
{"valid authorization", "Authorization", false},
73+
{"valid with numbers", "X-API-Key-123", false},
74+
{"valid with dots", "X.Custom.Header", false},
75+
76+
// CRLF injection attacks
77+
{"crlf injection", "X-API-Key\r\nX-Injected: malicious", true},
78+
{"newline injection", "X-API-Key\nInjected", true},
79+
{"carriage return", "X-API-Key\r", true},
80+
81+
// Other invalid characters
82+
{"null byte", "X-API-Key\x00", true},
83+
{"contains space", "X API Key", true},
84+
{"empty string", "", true},
85+
86+
// Length limits
87+
{"too long", strings.Repeat("A", 300), true},
88+
}
89+
90+
for _, tt := range tests {
91+
t.Run(tt.name, func(t *testing.T) {
92+
t.Parallel()
93+
err := validation.ValidateHTTPHeaderName(tt.input)
94+
if tt.expectErr {
95+
assert.Error(t, err)
96+
} else {
97+
assert.NoError(t, err)
98+
}
99+
})
100+
}
101+
}
102+
103+
func TestValidateHTTPHeaderValue(t *testing.T) {
104+
t.Parallel()
105+
106+
tests := []struct {
107+
name string
108+
input string
109+
expectErr bool
110+
}{
111+
// Valid cases
112+
{"valid simple", "my-api-key-12345", false},
113+
{"valid with spaces", "Bearer token123", false},
114+
{"valid special chars", "key!@#$%^&*()", false},
115+
116+
// CRLF injection attacks
117+
{"crlf injection", "key\r\nX-Injected: malicious", true},
118+
{"newline injection", "key\ninjected", true},
119+
{"carriage return", "key\r", true},
120+
121+
// Control characters
122+
{"null byte", "key\x00value", true},
123+
{"control char", "key\x01value", true},
124+
{"delete char", "key\x7Fvalue", true},
125+
{"tab allowed", "key\tvalue", false}, // Tab is allowed in values
126+
127+
// Length limits
128+
{"too long", strings.Repeat("A", 10000), true},
129+
{"empty string", "", true},
130+
}
131+
132+
for _, tt := range tests {
133+
t.Run(tt.name, func(t *testing.T) {
134+
t.Parallel()
135+
err := validation.ValidateHTTPHeaderValue(tt.input)
136+
if tt.expectErr {
137+
assert.Error(t, err)
138+
} else {
139+
assert.NoError(t, err)
140+
}
141+
})
142+
}
143+
}

0 commit comments

Comments
 (0)