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
5 changes: 5 additions & 0 deletions .changeset/swift-vans-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"github.com/livekit/protocol": patch
---

Switch to new header validation for SIP commands
109 changes: 30 additions & 79 deletions livekit/sip.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/xtwirp"
"golang.org/x/text/language"
)
Expand Down Expand Up @@ -237,37 +236,10 @@ func (p *SIPOutboundTrunkInfo) AsTrunkInfo() *SIPTrunkInfo {
}
}

var reHeaders = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9\-_]*$`)

func validateHeader(header string) error {
if !reHeaders.MatchString(header) {
return fmt.Errorf("invalid header name: %q", header)
}
return nil
}

func validateHeaderKeys(headers map[string]string) error {
for k := range headers {
if err := validateHeader(k); err != nil {
return err
}
}
return nil
}

func validateHeaderValues(headers map[string]string) error {
for _, v := range headers {
if err := validateHeader(v); err != nil {
return err
}
}
return nil
}

// validateHeaders makes sure header names/keys and values are per SIP specifications
func validateHeaders(headers map[string]string) error {
for headerName, headerValue := range headers {
if err := ValidateHeaderName(headerName); err != nil {
if err := ValidateHeaderName(headerName, true); err != nil {
return fmt.Errorf("invalid header name: %w", err)
}
if err := ValidateHeaderValue(headerName, headerValue); err != nil {
Expand All @@ -278,9 +250,19 @@ func validateHeaders(headers map[string]string) error {
}

// validateHeaderNames Makes sure the values of the given map correspond to valid SIP header names
func validateHeaderNames(attributesToHeaders map[string]string) error {
func validateAttributesToHeaders(attributesToHeaders map[string]string) error {
for _, headerName := range attributesToHeaders {
if err := ValidateHeaderName(headerName); err != nil {
if err := ValidateHeaderName(headerName, false); err != nil {
return fmt.Errorf("invalid header name: %w", err)
}
}
return nil
}

// validateHeaderToAttributes Makes sure the keys of the given map correspond to valid SIP header names
func validateHeaderToAttributes(headerToAttributes map[string]string) error {
for headerName, _ := range headerToAttributes {
if err := ValidateHeaderName(headerName, false); err != nil {
return fmt.Errorf("invalid header name: %w", err)
}
}
Expand Down Expand Up @@ -383,24 +365,15 @@ func (p *SIPInboundTrunkInfo) Validate() error {
if !hasAuth && !hasCIDR && !hasNumbers {
return errors.New("for security, one of the fields must be set: AuthUsername+AuthPassword, AllowedAddresses or Numbers")
}
if err := validateHeaderKeys(p.Headers); err != nil {
if err := validateHeaders(p.Headers); err != nil {
return err
}
if err := validateHeaderKeys(p.HeadersToAttributes); err != nil {
if err := validateAttributesToHeaders(p.AttributesToHeaders); err != nil {
return err
}
if err := validateHeaderValues(p.AttributesToHeaders); err != nil {
if err := validateHeaderToAttributes(p.HeadersToAttributes); err != nil {
return err
}
if err := validateHeaders(p.Headers); err != nil {
logger.Warnw("Header validation failed for Headers field", err)
// TODO: Once we're happy with the validation, we want this to error out
}
// Don't bother with HeadersToAttributes. If they're invalid, we just won't match
if err := validateHeaderNames(p.AttributesToHeaders); err != nil {
logger.Warnw("Header validation failed for AttributesToHeaders field", err)
// TODO: Once we're happy with the validation, we want this to error out
}
return nil
}

Expand Down Expand Up @@ -479,24 +452,15 @@ func (p *SIPOutboundTrunkInfo) Validate() error {
} else if strings.ContainsAny(p.Address, "@;") || strings.HasPrefix(p.Address, "sip:") || strings.HasPrefix(p.Address, "sips:") {
return errors.New("trunk address should be a hostname or IP, not SIP URI")
}
if err := validateHeaderKeys(p.Headers); err != nil {
if err := validateHeaders(p.Headers); err != nil {
return err
}
if err := validateHeaderKeys(p.HeadersToAttributes); err != nil {
if err := validateAttributesToHeaders(p.AttributesToHeaders); err != nil {
return err
}
if err := validateHeaderValues(p.AttributesToHeaders); err != nil {
if err := validateHeaderToAttributes(p.HeadersToAttributes); err != nil {
return err
}
if err := validateHeaders(p.Headers); err != nil {
logger.Warnw("Header validation failed for Headers field", err)
// TODO: Once we're happy with the validation, we want this to error out
}
// Don't bother with HeadersToAttributes. If they're invalid, we just won't match
if err := validateHeaderNames(p.AttributesToHeaders); err != nil {
logger.Warnw("Header validation failed for AttributesToHeaders field", err)
// TODO: Once we're happy with the validation, we want this to error out
}
return nil
}

Expand All @@ -508,17 +472,12 @@ func (p *SIPOutboundConfig) Validate() error {
} else if strings.ContainsAny(p.Hostname, "@;") || strings.HasPrefix(p.Hostname, "sip:") || strings.HasPrefix(p.Hostname, "sips:") {
return errors.New("trunk hostname should be a domain name or IP, not SIP URI")
}
if err := validateHeaderKeys(p.HeadersToAttributes); err != nil {
if err := validateAttributesToHeaders(p.AttributesToHeaders); err != nil {
return err
}
if err := validateHeaderValues(p.AttributesToHeaders); err != nil {
if err := validateHeaderToAttributes(p.HeadersToAttributes); err != nil {
return err
}
// Don't bother with HeadersToAttributes. If they're invalid, we just won't match
if err := validateHeaderNames(p.AttributesToHeaders); err != nil {
logger.Warnw("Header validation failed for AttributesToHeaders field", err)
// No error, just a warning for SIP RFC validation for now
}
return nil
}

Expand Down Expand Up @@ -720,29 +679,26 @@ func (p *CreateSIPParticipantRequest) Validate() error {
if p.RoomName == "" {
return errors.New("missing room name")
}
if err := validateHeaderKeys(p.Headers); err != nil {
return err
}

if err := validateHeaders(p.Headers); err != nil {
logger.Warnw("Header validation failed for Headers field", err)
// TODO: Once we're happy with the validation, we want this to error out
}

// Validate display_name if provided
if p.DisplayName != nil {
if len(*p.DisplayName) > 128 {
return errors.New("display_name too long (max 128 characters)")
return errors.New("DisplayName too long (max 128 characters)")
}
if _, err := strconv.Unquote("\"" + *p.DisplayName + "\""); err != nil {
return fmt.Errorf("DisplayName must be a valid quoted string: %w", err)
}

// TODO: Once we're happy with the validation, we want this to error out
}

// Validate destination if provided
if err := p.Destination.Validate(); err != nil {
return err
}

if err := validateHeaders(p.Headers); err != nil {
return err
}

return nil
}

Expand Down Expand Up @@ -823,13 +779,8 @@ func (p *TransferSIPParticipantRequest) Validate() error {
p.TransferTo = innerURI
}

if err := validateHeaderKeys(p.Headers); err != nil {
return err
}

if err := validateHeaders(p.Headers); err != nil {
logger.Warnw("Header validation failed for Headers field", err)
// TODO: Once we're happy with the validation, we want this to error out
return err
}

return nil
Expand Down
24 changes: 15 additions & 9 deletions livekit/sip_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
// RFC 3261 compliant validation functions for SIP headers and messages

type allowedCharacters struct {
ascii [127]bool
ascii [128]bool
utf8 bool
}

Expand Down Expand Up @@ -63,6 +63,7 @@ func (a *allowedCharacters) AddPrintableLienarASCII() {
for i := 0x20; i <= 0x7E; i++ {
a.ascii[i] = true
}
a.ascii[0x09] = true // \t or HTAB
}

func (a *allowedCharacters) Add(chars string) error {
Expand Down Expand Up @@ -94,7 +95,10 @@ func (a *allowedCharacters) Copy() *allowedCharacters {

func (a *allowedCharacters) Validate(target string) error {
for _, char := range target {
if int(char) >= len(a.ascii) && !a.utf8 {
if int(char) >= len(a.ascii) {
if a.utf8 {
continue
}
return fmt.Errorf("char %d out of range, consider explicilty adding utf8 characters", char)
}
if !a.ascii[char] {
Expand Down Expand Up @@ -141,7 +145,8 @@ func init() {
displayNameCharacters.Add(" \t")

headerValuesCharacters = NewAllowedCharacters()
headerValuesCharacters.AddPrintableLienarASCII() // Specifically not adding UTF8 for now
headerValuesCharacters.AddPrintableLienarASCII()
headerValuesCharacters.AddUTF8()
}

// Required headers for SIP requests per RFC 3261 Section 8.1.1
Expand Down Expand Up @@ -182,7 +187,6 @@ var FrobiddenSipHeaderNames = map[string]bool{
"max-forwards": true,
"record-route": true,
"refer-to": true, // rfc3515
"referred-by": true, // rfc3892sipUriCharacters
"reply-to": true,
"k": true, // Supported
"l": true, // Content-Length
Expand All @@ -208,7 +212,7 @@ var nameAddrHeaders = map[string]bool{
}

// ValidateHeaderName validates a SIP header name per RFC 3261 Section 25.1
func ValidateHeaderName(name string) error {
func ValidateHeaderName(name string, restrictNames bool) error {
if name == "" {
return errors.New("header name cannot be empty")
}
Expand All @@ -222,9 +226,11 @@ func ValidateHeaderName(name string) error {
}

// Convert to lowercase for case-insensitive comparison
lowerName := strings.ToLower(name)
if forbidden, exists := FrobiddenSipHeaderNames[lowerName]; exists && forbidden {
return fmt.Errorf("header name %s not supported", name)
if restrictNames {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gated behind a boolean check - we want to be able to specify "forbidden" headers in HeadersToAttributes and AttributesToHeaders, but not in Headers.

lowerName := strings.ToLower(name)
if forbidden, exists := FrobiddenSipHeaderNames[lowerName]; exists && forbidden {
return fmt.Errorf("header name %s not supported", name)
}
}

return nil
Expand All @@ -233,7 +239,7 @@ func ValidateHeaderName(name string) error {
// ValidateHeaderValue validates a SIP header value per RFC 3261 Section 25.1
func ValidateHeaderValue(name, value string) error {
if value == "" {
return fmt.Errorf("header %s: value cannot be empty", name)
return nil
}

if len(value) > 1024 {
Expand Down
18 changes: 8 additions & 10 deletions livekit/sip_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ var InvalidHeaderNames = []string{
// ValidHeaderValues contains valid SIP header values (implementation-specific restrictions)
// Note: These restrictions are NOT in RFC 3261 but are applied for security/performance
var ValidHeaderValues = []string{
"", // empty
"u1@example.com", // basic email
"<sip:u2@example.com>", // SIP URI with brackets
"Alice <sip:u3@example.com>", // display name + URI
Expand All @@ -94,24 +95,21 @@ var ValidHeaderValues = []string{
"text/plain; charset=utf-8", // Content-Type with params
"<sip:u5@[2001:db8::1]:5060>", // IPv6 URI
"\"Alice & Bob\" <sip:u6@example.com>", // display name with & symbol
"Header with\ttab", // tab (HTAB) per RFC 3261 Section 25.1
"Header with unicode café", // Unicode
"Header with unicode 世界", // Unicode
"Header with unicode émojis 🎉", // Unicode with emojis
strings.Repeat("a", 1024), // max length
}

// Note: These restrictions are NOT in RFC 3261 but are applied for security/performance
var InvalidHeaderValues = []string{
"", // empty
"Header with\nnewline", // newline
"Header with\rreturn", // carriage return
"Header with\ttab", // tab
"Header with\x00null", // null byte
"Header with\x01control", // control character
"Header with\x1Funit separator", // control character
"Header with\x7Fdelete", // delete character
"Header with\x80extended", // extended ASCII
"Header with\xFFextended", // extended ASCII
"Header with unicode café", // Unicode
"Header with unicode 世界", // Unicode
"Header with unicode émojis 🎉", // Unicode with emojis
strings.Repeat("a", 1025), // too long
}

Expand Down Expand Up @@ -176,7 +174,7 @@ var InvalidNameAddrHeaders = []string{
func TestValidateHeaderName_ValidHeaders(t *testing.T) {
for i, headerName := range ValidHeaderNames {
t.Run(testCaseName(headerName, 32, i), func(t *testing.T) {
err := ValidateHeaderName(headerName)
err := ValidateHeaderName(headerName, true)
if err != nil {
t.Errorf("ValidateHeaderName(%q) = %v, want nil", headerName, err)
}
Expand All @@ -188,7 +186,7 @@ func TestValidateHeaderName_ValidHeaders(t *testing.T) {
func TestValidateHeaderName_InvalidHeaders(t *testing.T) {
for i, headerName := range InvalidHeaderNames {
t.Run(testCaseName(headerName, 32, i), func(t *testing.T) {
err := ValidateHeaderName(headerName)
err := ValidateHeaderName(headerName, true)
if err == nil {
t.Errorf("ValidateHeaderName(%q) = nil, want error", headerName)
}
Expand Down Expand Up @@ -250,7 +248,7 @@ func TestFrobiddenSipHeaderNames(t *testing.T) {
for name := range FrobiddenSipHeaderNames {
i++
t.Run(testCaseName(name, 32, i), func(t *testing.T) {
err := ValidateHeaderName(name)
err := ValidateHeaderName(name, true)
if err == nil {
t.Errorf("ValidateHeaderName(%q) = nil, want error", name)
}
Expand Down
Loading