From 2e47657b7510c7b47fd5305896ed63ca4538594f Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 30 Oct 2025 14:04:32 -0700 Subject: [PATCH 1/5] fixes --- livekit/sip_validation.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/livekit/sip_validation.go b/livekit/sip_validation.go index 56386e7a9..313a46fe3 100644 --- a/livekit/sip_validation.go +++ b/livekit/sip_validation.go @@ -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 { @@ -182,7 +183,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 @@ -233,7 +233,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 { From cfdb2697f44746acb52620351acccc03da19db1d Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 30 Oct 2025 14:25:11 -0700 Subject: [PATCH 2/5] Add utf-8 --- livekit/sip_validation.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/livekit/sip_validation.go b/livekit/sip_validation.go index 313a46fe3..8c2e651a5 100644 --- a/livekit/sip_validation.go +++ b/livekit/sip_validation.go @@ -95,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] { @@ -142,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 From 07a7b264f30f562e0f0449d5fcc221ffd5a5d41b Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 30 Oct 2025 14:34:48 -0700 Subject: [PATCH 3/5] Tests okay --- livekit/sip_validation.go | 2 +- livekit/sip_validation_test.go | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/livekit/sip_validation.go b/livekit/sip_validation.go index 8c2e651a5..b5e086444 100644 --- a/livekit/sip_validation.go +++ b/livekit/sip_validation.go @@ -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 } diff --git a/livekit/sip_validation_test.go b/livekit/sip_validation_test.go index 9c9be2831..b85c64b7c 100644 --- a/livekit/sip_validation_test.go +++ b/livekit/sip_validation_test.go @@ -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 URI with brackets "Alice ", // display name + URI @@ -94,24 +95,21 @@ var ValidHeaderValues = []string{ "text/plain; charset=utf-8", // Content-Type with params "", // IPv6 URI "\"Alice & Bob\" ", // 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 } From f35eb0ebf98a00c385dfb64920567f96bb68fe6a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 30 Oct 2025 15:11:46 -0700 Subject: [PATCH 4/5] Changed validation to hard fail --- livekit/sip.go | 109 +++++++++------------------------ livekit/sip_validation.go | 10 +-- livekit/sip_validation_test.go | 6 +- 3 files changed, 39 insertions(+), 86 deletions(-) diff --git a/livekit/sip.go b/livekit/sip.go index 2832e838f..041b235fc 100644 --- a/livekit/sip.go +++ b/livekit/sip.go @@ -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" ) @@ -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 { @@ -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) } } @@ -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 } @@ -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 } @@ -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 } @@ -720,22 +679,15 @@ 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 @@ -743,6 +695,10 @@ func (p *CreateSIPParticipantRequest) Validate() error { return err } + if err := validateHeaders(p.Headers); err != nil { + return err + } + return nil } @@ -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 diff --git a/livekit/sip_validation.go b/livekit/sip_validation.go index b5e086444..faefbd27d 100644 --- a/livekit/sip_validation.go +++ b/livekit/sip_validation.go @@ -212,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") } @@ -226,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 { + lowerName := strings.ToLower(name) + if forbidden, exists := FrobiddenSipHeaderNames[lowerName]; exists && forbidden { + return fmt.Errorf("header name %s not supported", name) + } } return nil diff --git a/livekit/sip_validation_test.go b/livekit/sip_validation_test.go index b85c64b7c..89e904873 100644 --- a/livekit/sip_validation_test.go +++ b/livekit/sip_validation_test.go @@ -174,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) } @@ -186,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) } @@ -248,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) } From ea88da0eb39e4964f661ba311404667c64993b5a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 30 Oct 2025 15:27:51 -0700 Subject: [PATCH 5/5] + changeset --- .changeset/swift-vans-sneeze.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/swift-vans-sneeze.md diff --git a/.changeset/swift-vans-sneeze.md b/.changeset/swift-vans-sneeze.md new file mode 100644 index 000000000..9406bfb28 --- /dev/null +++ b/.changeset/swift-vans-sneeze.md @@ -0,0 +1,5 @@ +--- +"github.com/livekit/protocol": patch +--- + +Switch to new header validation for SIP commands