Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions pkg/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"regexp"
"strings"

"golang.org/x/net/http/httpguts"
)

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

return nil
}

// ValidateHTTPHeaderName validates that a string is a valid HTTP header name per RFC 7230.
// It checks for CRLF injection, control characters, and ensures RFC token compliance.
func ValidateHTTPHeaderName(name string) error {
if name == "" {
return fmt.Errorf("header name cannot be empty")
}

// Length limit to prevent DoS
if len(name) > 256 {
return fmt.Errorf("header name exceeds maximum length of 256 bytes")
}

// Use httpguts validation (same as Go's HTTP/2 implementation)
if !httpguts.ValidHeaderFieldName(name) {
return fmt.Errorf("invalid HTTP header name: contains invalid characters")
}

return nil
}

// ValidateHTTPHeaderValue validates that a string is a valid HTTP header value per RFC 7230.
// It checks for CRLF injection and control characters.
func ValidateHTTPHeaderValue(value string) error {
if value == "" {
return fmt.Errorf("header value cannot be empty")
}

// Length limit to prevent DoS (common HTTP server limit)
if len(value) > 8192 {
return fmt.Errorf("header value exceeds maximum length of 8192 bytes")
}

// Use httpguts validation
if !httpguts.ValidHeaderFieldValue(value) {
return fmt.Errorf("invalid HTTP header value: contains control characters")
}

return nil
}
84 changes: 84 additions & 0 deletions pkg/validation/validation_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package validation_test

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -57,3 +58,86 @@ func TestValidateGroupName(t *testing.T) {
})
}
}

func TestValidateHTTPHeaderName(t *testing.T) {
t.Parallel()

tests := []struct {
name string
input string
expectErr bool
}{
// Valid cases
{"valid simple", "X-API-Key", false},
{"valid authorization", "Authorization", false},
{"valid with numbers", "X-API-Key-123", false},
{"valid with dots", "X.Custom.Header", false},

// CRLF injection attacks
{"crlf injection", "X-API-Key\r\nX-Injected: malicious", true},
{"newline injection", "X-API-Key\nInjected", true},
{"carriage return", "X-API-Key\r", true},

// Other invalid characters
{"null byte", "X-API-Key\x00", true},
{"contains space", "X API Key", true},
{"empty string", "", true},

// Length limits
{"too long", strings.Repeat("A", 300), true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := validation.ValidateHTTPHeaderName(tt.input)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

func TestValidateHTTPHeaderValue(t *testing.T) {
t.Parallel()

tests := []struct {
name string
input string
expectErr bool
}{
// Valid cases
{"valid simple", "my-api-key-12345", false},
{"valid with spaces", "Bearer token123", false},
{"valid special chars", "key!@#$%^&*()", false},

// CRLF injection attacks
{"crlf injection", "key\r\nX-Injected: malicious", true},
{"newline injection", "key\ninjected", true},
{"carriage return", "key\r", true},

// Control characters
{"null byte", "key\x00value", true},
{"control char", "key\x01value", true},
{"delete char", "key\x7Fvalue", true},
{"tab allowed", "key\tvalue", false}, // Tab is allowed in values

// Length limits
{"too long", strings.Repeat("A", 10000), true},
{"empty string", "", true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := validation.ValidateHTTPHeaderValue(tt.input)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
Loading