From 86e7c8550b3386dbcd0fb8be1711242bf2b9944d Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Oct 2025 21:16:53 -0700 Subject: [PATCH 01/11] Validation is there --- livekit/sip_validation.go | 341 +++++++++++++++++++++++++++++++++ livekit/sip_validation_test.go | 259 +++++++++++++++++++++++++ 2 files changed, 600 insertions(+) create mode 100644 livekit/sip_validation.go create mode 100644 livekit/sip_validation_test.go diff --git a/livekit/sip_validation.go b/livekit/sip_validation.go new file mode 100644 index 00000000..77500861 --- /dev/null +++ b/livekit/sip_validation.go @@ -0,0 +1,341 @@ +// Copyright 2023 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package livekit + +import ( + "errors" + "regexp" + "strings" +) + +// RFC 3261 compliant validation functions for SIP headers and messages + +// RFC 3261 Section 25.1 - Header field names +// token = 1*(alphanum / "-" / "." / "!" / "%" / "*" / "_" / "+" / "`" / "'" / "~") +var reHeaderName = regexp.MustCompile(`^[a-zA-Z0-9\-\.!%*_+` + "`" + `'~]+$`) + +// RFC 3261 Section 25.1 - Header field values (basic validation) +// More specific validation is done per header type +var reHeaderValueBasic = regexp.MustCompile(`^[\x20-\x7E]*$`) + +// RFC 3261 Section 19.1 - SIP URI validation +var reSIPURI = regexp.MustCompile(`^(sip|sips):([^@]+@)?([^;]+)(;.*)?$`) + +// Required headers for SIP requests per RFC 3261 Section 8.1.1 +var requiredRequestHeaders = map[string]bool{ + "Via": true, + "From": true, + "To": true, + "Call-ID": true, + "CSeq": true, + "Max-Forwards": true, +} + +// Required headers for SIP responses per RFC 3261 Section 8.2.1 +var requiredResponseHeaders = map[string]bool{ + "Via": true, + "From": true, + "To": true, + "Call-ID": true, + "CSeq": true, +} + +// Headers that must comply with name-addr specification per RFC 3261 Section 20.10 +// name-addr = [display-name] +// addr-spec = SIP-URI / SIPS-URI / absoluteURI +var nameAddrHeaders = map[string]bool{ + "From": true, + "To": true, + "Contact": true, + "Route": true, + "Record-Route": true, + "Reply-To": true, + "P-Asserted-Identity": true, // RFC 3325 Section 9.1 +} + +// ValidateHeaderName validates a SIP header name per RFC 3261 Section 25.1 +func ValidateHeaderName(name string) error { + if name == "" { + return errors.New("header name cannot be empty") + } + + if len(name) > 255 { + return errors.New("header name too long (max 255 characters)") + } + + if !reHeaderName.MatchString(name) { + return errors.New("header name contains invalid characters") + } + + return nil +} + +// ValidateHeaderValue validates a SIP header value per RFC 3261 Section 25.1 +func ValidateHeaderValue(name, value string) error { + if value == "" { + return errors.New("header value cannot be empty") + } + + if len(value) > 1024 { + return errors.New("header value too long (max 1024 characters)") + } + + // Basic character validation - printable ASCII + if !reHeaderValueBasic.MatchString(value) { + return errors.New("header value contains invalid characters") + } + + if _, exists := nameAddrHeaders[name]; exists { + return validateNameAddrHeader(value) + } + + return nil +} + +// findAngleBrackets efficiently finds angle brackets in a single scan +// Returns: start, end positions (-1 = missing), and error status +func findAngleBrackets(value string) (int, int, error) { + start := -1 + end := -1 + + for i, r := range value { + if r == '<' { + if start != -1 { + return -1, -1, errors.New("multiple opening brackets") + } + start = i + } else if r == '>' { + if end != -1 { + return -1, -1, errors.New("multiple closing brackets") + } + end = i + } + } + + // Check for mismatched brackets + if (start == -1) != (end == -1) { + return -1, -1, errors.New("mismatched angle brackets") + } + + // Check that < comes before > + if start > end { + return -1, -1, errors.New("malformed angle brackets") + } + + return start, end, nil +} + +// validateNameAddrHeader validates headers that use name-addr format per RFC 3261 Section 20.10 +func validateNameAddrHeader(value string) error { + // RFC 3261 Section 20.10 - name-addr format + // name-addr = [display-name] + // addr-spec = SIP-URI / SIPS-URI / absoluteURI + + start, end, err := findAngleBrackets(value) + if err != nil { + return err + } + if start >= 0 || end >= 0 { + displayName := strings.TrimSpace(value[:start]) + uri := value[start+1 : end] + if displayName != "" { + if err := validateDisplayName(displayName); err != nil { + return err + } + } + // Keep in mind, this ignores header parameters, while validating URI parameters + return validateURI(uri) + } + + // This is a bare URI, and should comply with addr-spec, no special characters + if strings.ContainsAny(value, ";,? ") { + return errors.New("Bare URI with special characters must be enclosed in angle brackets") + } + + return validateURI(value) +} + +// validateDisplayName validates a display name in name-addr format +func validateDisplayName(displayName string) error { + if displayName == "" { + return nil + } + + // Check if display name is quoted + if strings.HasPrefix(displayName, "\"") && strings.HasSuffix(displayName, "\"") { + // Quoted display name - basic validation + quoted := displayName[1 : len(displayName)-1] + // Check for proper escaping + if strings.Contains(quoted, "\"") && !strings.Contains(quoted, "\\\"") { + return errors.New("unquoted display name contains unescaped quotes") + } + return nil + } + + // Unquoted display name - must not contain special characters + for _, r := range displayName { + if r == '<' || r == '>' || r == '"' || r == '\\' || r == '&' { + return errors.New("unquoted display name contains special characters") + } + } + + return nil +} + +// validateURI validates URIs that can appear in name-addr format +func validateURI(uri string) error { + if uri == "" { + return errors.New("URI cannot be empty") + } + + // Check for SIP/SIPS scheme + if strings.HasPrefix(uri, "sip:") || strings.HasPrefix(uri, "sips:") { + return validateSIPURI(uri) + } + + // Check for TEL scheme + if strings.HasPrefix(uri, "tel:") { + return validateTELURI(uri) + } + + // For now, only support SIP/SIPS and TEL URIs + // RFC 3261 allows other absolute URIs, but we'll be restrictive + return errors.New("URI must be sip:, sips:, or tel:") +} + +// validateSIPURI validates a SIP or SIPS URI per RFC 3261 Section 19.1 +func validateSIPURI(uri string) error { + if uri == "" { + return errors.New("SIP URI cannot be empty") + } + + // Check for SIP or SIPS scheme + if !strings.HasPrefix(uri, "sip:") && !strings.HasPrefix(uri, "sips:") { + return errors.New("SIP URI must start with sip: or sips:") + } + + // Basic format validation + if !reSIPURI.MatchString(uri) { + return errors.New("SIP URI format is invalid") + } + + // Validate URI parameters if present + if strings.Contains(uri, ";") { + return validateURIParameters(uri) + } + + return nil +} + +// validateURIParameters validates URI parameters +func validateURIParameters(uri string) error { + // Split URI into base and parameters + parts := strings.Split(uri, ";") + if len(parts) < 2 { + return errors.New("invalid URI parameters") + } + + // Validate each parameter + for _, param := range parts[1:] { + param = strings.TrimSpace(param) + if param == "" { + return errors.New("empty URI parameter") + } + + // Check for valid parameter format: name=value or name + if strings.Contains(param, "=") { + paramParts := strings.SplitN(param, "=", 2) + if len(paramParts) != 2 { + return errors.New("invalid URI parameter format") + } + name := strings.TrimSpace(paramParts[0]) + value := strings.TrimSpace(paramParts[1]) + + if name == "" { + return errors.New("URI parameter name cannot be empty") + } + if value == "" { + return errors.New("URI parameter value cannot be empty") + } + + } else { + // Parameter without value - just validate name + if strings.TrimSpace(param) == "" { + return errors.New("URI parameter name cannot be empty") + } + } + + // Check for invalid characters in parameter + if strings.Contains(param, " ") { + return errors.New("URI parameter contains spaces") + } + } + + return nil +} + +// validateTELURI validates a TEL URI per RFC 3966 +func validateTELURI(uri string) error { + if uri == "" { + return errors.New("TEL URI cannot be empty") + } + + if !strings.HasPrefix(uri, "tel:") { + return errors.New("TEL URI must start with tel:") + } + + // Basic validation - TEL URIs are more complex, this is simplified + if len(uri) < 5 { // "tel:" + at least one character + return errors.New("TEL URI format is invalid") + } + + return nil +} + +// EscapeHeaderValue escapes special characters in header values per RFC 3261 +func EscapeHeaderValue(value string) string { + // Escape special characters that need to be quoted + var result strings.Builder + for _, r := range value { + switch r { + case ' ', '\t', '\r', '\n', '"', '\\': + // These characters need to be escaped or quoted + result.WriteString("\\") + result.WriteRune(r) + default: + result.WriteRune(r) + } + } + return result.String() +} + +// UnescapeHeaderValue removes escaping from header values +func UnescapeHeaderValue(value string) string { + var result strings.Builder + escaped := false + + for _, r := range value { + if escaped { + result.WriteRune(r) + escaped = false + } else if r == '\\' { + escaped = true + } else { + result.WriteRune(r) + } + } + + return result.String() +} diff --git a/livekit/sip_validation_test.go b/livekit/sip_validation_test.go new file mode 100644 index 00000000..3628ab3e --- /dev/null +++ b/livekit/sip_validation_test.go @@ -0,0 +1,259 @@ +// Copyright 2023 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package livekit + +import ( + "fmt" + "strings" + "testing" +) + +// Valid Header Test Cases + +// ValidHeaderNames contains valid SIP header names +var ValidHeaderNames = []string{ + "A", // single letter + "a", // lowercase + "F", // single uppercase + "f", // single lowercase + "From", // keyword + "Call-ID", // hyphenated keyword + "P-Asserted-Identity", // multiple hyphens + "X-", // hyphen at end + "-X", // hyphen at start + "X123", // alphanumeric + "X_123", // underscore + "X.123", // period + "X!123", // exclamation + "X%123", // percent + "X*123", // asterisk + "X+123", // plus + "X`123", // backtick + "X'123", // single quote + "X~123", // tilde +} + +// InvalidHeaderNames contains invalid SIP header names +var InvalidHeaderNames = []string{ + "", // empty + "From To", // space in name + "From:To", // colon in name + "From,To", // comma in name + "From;To", // semicolon in name + "FromTo", // angle bracket in name + "From@To", // at symbol in name + "From\"To", // quote in name + "From\\To", // backslash in name + "From/To", // forward slash + "From[To", // square bracket + "From]To", // square bracket + "From{To", // curly brace + "From}To", // curly brace + "From(To", // parenthesis + "From)To", // parenthesis + "From?To", // question mark + "From=To", // equals sign + "From#To", // hash + "From$To", // dollar sign + "From&To", // ampersand + "From|To", // pipe + "From^To", // caret + "From\000To", // null byte + "From\nTo", // newline + "From\rTo", // carriage return + "From\tTo", // tab +} + +// 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{ + "alice@example.com", // basic email + "", // SIP URI with brackets + "Alice ", // display name + URI + "\"Alice Smith\" ", // quoted display name + "SIP/2.0/UDP 192.168.1.1:5060", // Via header + "1 INVITE", // CSeq header + "255", // Max-Forwards (max valid) + "0", // Max-Forwards (min valid) + "application/sdp", // Content-Type + "123", // Content-Length + "3600", // Expires + "call-123@example.com", // Call-ID + "text/plain; charset=utf-8", // Content-Type with params + "", // IPv6 URI + "\"Alice & Bob\" ", // display name with & symbol + strings.Repeat("a", 1024), // max lenth +} + +// 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 +} + +// ValidDisplayNames contains valid display name formats +var ValidDisplayNames = []string{ + `"Alex Dev M" `, + `"Alex's \"Dev\" M\\" `, + `Alex Developer `, +} + +// InvalidDisplayNames contains invalid display name formats +var InvalidDisplayNames = []string{ + `"Alex "Dev" M" `, // unescaped quotes + `"Alex \M" `, // unescaped backslashes + `"Alex Developer" M `, // unmatched quotes + `Alex "Developer" M `, // unescaped quotes in unquoted + `"Alex Developer M `, // unterminated quote + `Alex Developer M" `, // unmatched quote +} + +// testCaseName truncates a test case name to maxLen and adds dots with total size +func testCaseName(name string, maxLen int, index int) string { + if len(name) <= maxLen { + return fmt.Sprintf("%d/%s)", index+1, name) + } + // Truncate to make room for "..." and size info + truncated := name[:maxLen-10] // Reserve space for "..." and "(1234)" + return fmt.Sprintf("%d/%s...(%d)", index+1, truncated, len(name)) +} + +// ValidNameAddrHeaders contains valid Name-addr format headers with parameters +var ValidNameAddrHeaders = []string{ + "sip:a1@example.com", // basic SIP URI (no brackets needed) + "sips:a2@example.com", // secure SIP URI (no brackets needed) + "tel:+1-555-123-4567", // TEL URI (no brackets needed) + "", // basic SIP URI with brackets + "", // secure SIP URI with brackets + "", // TEL URI with brackets + "Alice ", // display name + SIP URI + "\"Alice Smith\" ", // quoted display name + "", // SIP URI with transport + "", // SIP URI with flag param + "", // SIP URI with port + "", // SIP URI with multiple params + "Alice ", // display name + params + "\"Alice \\\"Quote\\\"\" ", // quoted display + "", // IPv6 with params + ";expires=60", // SIPS URI with expires parameter + "Alice ", // display name + params + "\"Alice & Bob\" ", // display name with & symbol +} + +// InvalidNameAddrHeaders contains invalid Name-addr format headers +var InvalidNameAddrHeaders = []string{ + "", // missing opening bracket + " ", // multiple URIs + "Alice ", // multiple URIs with display + "Alice sip:u7@example.com", // display name without brackets + "Alice sips:u8@example.com", // display name without brackets + "Alice&Bob ", // display name with & symbol + "sip:u10@example.com;transport=tcp", // special chars without brackets + "sip:u11@example.com,transport=tcp", // comma without brackets + "sip:u12@example.com?transport=tcp", // question mark without brackets + "", // empty parameter value + "", // empty parameter name + "", // missing equals sign + "", // trailing semicolon + "", // double semicolon + "", // space in parameters +} + +// TestValidateHeaderName_ValidHeaders tests that all valid header names pass validation +func TestValidateHeaderName_ValidHeaders(t *testing.T) { + for i, headerName := range ValidHeaderNames { + t.Run(testCaseName(headerName, 32, i), func(t *testing.T) { + err := ValidateHeaderName(headerName) + if err != nil { + t.Errorf("ValidateHeaderName(%q) = %v, want nil", headerName, err) + } + }) + } +} + +// TestValidateHeaderName_InvalidHeaders tests that all invalid header names fail validation +func TestValidateHeaderName_InvalidHeaders(t *testing.T) { + for i, headerName := range InvalidHeaderNames { + t.Run(testCaseName(headerName, 32, i), func(t *testing.T) { + err := ValidateHeaderName(headerName) + if err == nil { + t.Errorf("ValidateHeaderName(%q) = nil, want error", headerName) + } + }) + } +} + +// TestValidateHeaderValue_ValidValues tests that all valid header values pass validation +func TestValidateHeaderValue_ValidValues(t *testing.T) { + for i, headerValue := range ValidHeaderValues { + t.Run(testCaseName(headerValue, 32, i), func(t *testing.T) { + err := ValidateHeaderValue("Test-Header", headerValue) + if err != nil { + t.Errorf("ValidateHeaderValue(%q) = %v, want nil", headerValue, err) + } + }) + } +} + +// TestValidateHeaderValue_InvalidValues tests that all invalid header values fail validation +// Note: These restrictions are implementation-specific, NOT from RFC 3261 +func TestValidateHeaderValue_InvalidValues(t *testing.T) { + for i, headerValue := range InvalidHeaderValues { + t.Run(testCaseName(headerValue, 32, i), func(t *testing.T) { + err := ValidateHeaderValue("Test-Header", headerValue) + if err == nil { + t.Errorf("ValidateHeaderValue(%q) = nil, want error", headerValue) + } + }) + } +} + +// TestValidateNameAddr_ValidHeaders tests that all valid Name-addr headers pass validation +func TestValidateNameAddr_ValidHeaders(t *testing.T) { + for i, nameAddr := range ValidNameAddrHeaders { + t.Run(testCaseName(nameAddr, 32, i), func(t *testing.T) { + err := validateNameAddrHeader(nameAddr) + if err != nil { + t.Errorf("validateNameAddrHeader(%q) = %v, want nil", nameAddr, err) + } + }) + } +} + +// TestValidateNameAddr_InvalidHeaders tests that all invalid Name-addr headers fail validation +func TestValidateNameAddr_InvalidHeaders(t *testing.T) { + for i, nameAddr := range InvalidNameAddrHeaders { + t.Run(testCaseName(nameAddr, 32, i), func(t *testing.T) { + err := validateNameAddrHeader(nameAddr) + if err == nil { + t.Errorf("validateNameAddrHeader(%q) = nil, want error", nameAddr) + } + }) + } +} From 7fbabf2cc7504d3083a12c1e90b9189c389502ac Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Oct 2025 22:59:16 -0700 Subject: [PATCH 02/11] :Validation and forbidden stuff in place --- livekit/sip_validation.go | 112 ++++++++++++++++++++++++--------- livekit/sip_validation_test.go | 24 +++++-- 2 files changed, 101 insertions(+), 35 deletions(-) diff --git a/livekit/sip_validation.go b/livekit/sip_validation.go index 77500861..5c44e27f 100644 --- a/livekit/sip_validation.go +++ b/livekit/sip_validation.go @@ -16,6 +16,7 @@ package livekit import ( "errors" + fmt "fmt" "regexp" "strings" ) @@ -24,7 +25,8 @@ import ( // RFC 3261 Section 25.1 - Header field names // token = 1*(alphanum / "-" / "." / "!" / "%" / "*" / "_" / "+" / "`" / "'" / "~") -var reHeaderName = regexp.MustCompile(`^[a-zA-Z0-9\-\.!%*_+` + "`" + `'~]+$`) +// Specifically lowercase since we're converting to lowercase for case-insensitive comparison +var reHeaderName = regexp.MustCompile(`^[a-z0-9\-\.!%*_+` + "`" + `'~]+$`) // RFC 3261 Section 25.1 - Header field values (basic validation) // More specific validation is done per header type @@ -34,35 +36,77 @@ var reHeaderValueBasic = regexp.MustCompile(`^[\x20-\x7E]*$`) var reSIPURI = regexp.MustCompile(`^(sip|sips):([^@]+@)?([^;]+)(;.*)?$`) // Required headers for SIP requests per RFC 3261 Section 8.1.1 -var requiredRequestHeaders = map[string]bool{ - "Via": true, - "From": true, - "To": true, - "Call-ID": true, - "CSeq": true, - "Max-Forwards": true, +var RequiredRequestHeaders = map[string]bool{ + "via": true, + "from": true, + "to": true, + "call-id": true, + "cseq": true, + "max-forwards": true, } // Required headers for SIP responses per RFC 3261 Section 8.2.1 -var requiredResponseHeaders = map[string]bool{ - "Via": true, - "From": true, - "To": true, - "Call-ID": true, - "CSeq": true, +var RequiredResponseHeaders = map[string]bool{ + "via": true, + "from": true, + "to": true, + "call-id": true, + "cseq": true, +} + +// Crucial headers that can't be overridden by the user, and their shorthands +var FrobiddenSipHeaderNames = map[string]bool{ + "accept": true, + "accept-encoding": true, + "accept-language": true, + "allow": true, + "allow-events": true, // rfc3903 + "call-id": true, + "contact": true, + "content-encoding": true, + "content-length": true, + "content-type": true, + "cseq": true, + "event": true, // rfc3903 + "expires": true, + "from": true, // We might allow this in the future, but for now we're printing + "max-forwards": true, + "record-route": true, + "refer-to": true, // rfc3515 + "referred-by": true, // rfc3892 + "reply-to": true, + "route": true, + "supported": true, + "to": true, // We might allow this in the future, but for now we're printing + "via": true, + + // Single-letter shorthands, a.k.a compact form + "b": true, // Referred-By; rfc3892 + "c": true, // Content-Type + "e": true, // Content-Encoding + "f": true, // From + "i": true, // Call-ID + "k": true, // Supported + "l": true, // Content-Length + "m": true, // Contact + "o": true, // Event; rfc3903 + "r": true, // Refer-To; rfc3515 + "t": true, // To + "u": true, // Allow-Events; rfc3903 + "v": true, // Via } // Headers that must comply with name-addr specification per RFC 3261 Section 20.10 // name-addr = [display-name] // addr-spec = SIP-URI / SIPS-URI / absoluteURI var nameAddrHeaders = map[string]bool{ - "From": true, - "To": true, - "Contact": true, - "Route": true, - "Record-Route": true, - "Reply-To": true, - "P-Asserted-Identity": true, // RFC 3325 Section 9.1 + "from": true, + "to": true, + "contact": true, + "route": true, + "record-route": true, + "reply-to": true, + "p-asserted-identity": true, // RFC 3325 Section 9.1 } // ValidateHeaderName validates a SIP header name per RFC 3261 Section 25.1 @@ -75,10 +119,16 @@ func ValidateHeaderName(name string) error { return errors.New("header name too long (max 255 characters)") } - if !reHeaderName.MatchString(name) { + lowerName := strings.ToLower(name) + if !reHeaderName.MatchString(lowerName) { return errors.New("header name contains invalid characters") } + // Convert to lowercase for case-insensitive comparison + if forbidden, exists := FrobiddenSipHeaderNames[lowerName]; exists && forbidden { + return fmt.Errorf("header name %s not supported", name) + } + return nil } @@ -97,7 +147,10 @@ func ValidateHeaderValue(name, value string) error { return errors.New("header value contains invalid characters") } - if _, exists := nameAddrHeaders[name]; exists { + // Convert to lowercase for case-insensitive comparison + lowerName := strings.ToLower(name) + if _, exists := nameAddrHeaders[lowerName]; exists && false { + // TODO: Disabled since all supported headers are forbidden, re-enable when we allow some return validateNameAddrHeader(value) } @@ -111,12 +164,13 @@ func findAngleBrackets(value string) (int, int, error) { end := -1 for i, r := range value { - if r == '<' { + switch r { + case '<': if start != -1 { return -1, -1, errors.New("multiple opening brackets") } start = i - } else if r == '>' { + case '>': if end != -1 { return -1, -1, errors.New("multiple closing brackets") } @@ -161,7 +215,7 @@ func validateNameAddrHeader(value string) error { // This is a bare URI, and should comply with addr-spec, no special characters if strings.ContainsAny(value, ";,? ") { - return errors.New("Bare URI with special characters must be enclosed in angle brackets") + return errors.New("bare URI with special characters") } return validateURI(value) @@ -212,7 +266,7 @@ func validateURI(uri string) error { // For now, only support SIP/SIPS and TEL URIs // RFC 3261 allows other absolute URIs, but we'll be restrictive - return errors.New("URI must be sip:, sips:, or tel:") + return errors.New("URI scheme must match one of sip, sips, or tel") } // validateSIPURI validates a SIP or SIPS URI per RFC 3261 Section 19.1 @@ -223,7 +277,7 @@ func validateSIPURI(uri string) error { // Check for SIP or SIPS scheme if !strings.HasPrefix(uri, "sip:") && !strings.HasPrefix(uri, "sips:") { - return errors.New("SIP URI must start with sip: or sips:") + return errors.New("SIP URI scheme must match sip or sips") } // Basic format validation @@ -293,7 +347,7 @@ func validateTELURI(uri string) error { } if !strings.HasPrefix(uri, "tel:") { - return errors.New("TEL URI must start with tel:") + return errors.New("TEL URI scheme must match tel") } // Basic validation - TEL URIs are more complex, this is simplified diff --git a/livekit/sip_validation_test.go b/livekit/sip_validation_test.go index 3628ab3e..de7075e3 100644 --- a/livekit/sip_validation_test.go +++ b/livekit/sip_validation_test.go @@ -24,12 +24,11 @@ import ( // ValidHeaderNames contains valid SIP header names var ValidHeaderNames = []string{ - "A", // single letter - "a", // lowercase - "F", // single uppercase - "f", // single lowercase - "From", // keyword - "Call-ID", // hyphenated keyword + "Q", // single uppercase + "q", // single lowercase + "Qrom", // keyword + "qrom", // keyword + "Qall-ID", // hyphenated keyword "P-Asserted-Identity", // multiple hyphens "X-", // hyphen at end "-X", // hyphen at start @@ -257,3 +256,16 @@ func TestValidateNameAddr_InvalidHeaders(t *testing.T) { }) } } + +func TestFrobiddenSipHeaderNames(t *testing.T) { + i := 0 + for name := range FrobiddenSipHeaderNames { + i++ + t.Run(testCaseName(name, 32, i), func(t *testing.T) { + err := ValidateHeaderName(name) + if err == nil { + t.Errorf("ValidateHeaderName(%q) = nil, want error", name) + } + }) + } +} From 19f861565868826c6423c0a627bf7c7f4ad90577 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Oct 2025 23:48:37 -0700 Subject: [PATCH 03/11] Initial WIP --- livekit/sip.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/livekit/sip.go b/livekit/sip.go index ea7d531c..4a36c5e5 100644 --- a/livekit/sip.go +++ b/livekit/sip.go @@ -263,6 +263,29 @@ func validateHeaderValues(headers map[string]string) error { 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 { + return fmt.Errorf("invalid header name: %w", err) + } + if err := ValidateHeaderValue(headerName, headerValue); err != nil { + return fmt.Errorf("invalid header value for %s: %w", headerName, err) + } + } + return nil +} + +// validateHeaderNames Makes sure the values of the given map correspond to valid SIP header names +func validateHeaderNames(attributesToHeaders map[string]string) error { + for _, headerName := range attributesToHeaders { + if err := ValidateHeaderName(headerName); err != nil { + return fmt.Errorf("invalid header name: %w", err) + } + } + return nil +} + func (p *SIPTrunkInfo) Validate() error { if len(p.InboundNumbersRegex) != 0 { return fmt.Errorf("trunks with InboundNumbersRegex are deprecated") @@ -368,6 +391,15 @@ func (p *SIPInboundTrunkInfo) Validate() error { if err := validateHeaderValues(p.AttributesToHeaders); err != nil { return err } + if err := validateHeaders(p.Headers); err != nil { + fmt.Printf("Warning: Header validation failed for Headers field: %v\n", err) + // No error, just a warning for SIP RFC validation for now + } + // Don't bother with HeadersToAttributes. If they're invalid, we just won't match + if err := validateHeaderNames(p.AttributesToHeaders); err != nil { + fmt.Printf("Warning: Header validation failed for AttributesToHeaders field: %v\n", err) + // No error, just a warning for SIP RFC validation for now + } return nil } @@ -455,6 +487,15 @@ func (p *SIPOutboundTrunkInfo) Validate() error { if err := validateHeaderValues(p.AttributesToHeaders); err != nil { return err } + if err := validateHeaders(p.Headers); err != nil { + fmt.Printf("Warning: Header validation failed for Headers field: %v\n", err) + // No error, just a warning for SIP RFC validation for now + } + // Don't bother with HeadersToAttributes. If they're invalid, we just won't match + if err := validateHeaderNames(p.AttributesToHeaders); err != nil { + fmt.Printf("Warning: Header validation failed for AttributesToHeaders field: %v\n", err) + // No error, just a warning for SIP RFC validation for now + } return nil } @@ -472,6 +513,11 @@ func (p *SIPOutboundConfig) Validate() error { if err := validateHeaderValues(p.AttributesToHeaders); 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 { + fmt.Printf("Warning: Header validation failed for AttributesToHeaders field: %v\n", err) + // No error, just a warning for SIP RFC validation for now + } return nil } @@ -677,6 +723,11 @@ func (p *CreateSIPParticipantRequest) Validate() error { return err } + if err := validateHeaders(p.Headers); err != nil { + fmt.Printf("Warning: Header validation failed for Headers field: %v\n", err) + // No error, just a warning for SIP RFC validation for now + } + // Validate display_name if provided if p.DisplayName != nil { if len(*p.DisplayName) > 128 { @@ -775,6 +826,11 @@ func (p *TransferSIPParticipantRequest) Validate() error { return err } + if err := validateHeaders(p.Headers); err != nil { + fmt.Printf("Warning: Header validation failed for Headers field: %v\n", err) + // No error, just a warning for SIP RFC validation for now + } + return nil } From 65b9fcfc8fab8e2cf97d4274669dbc1109a74888 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Oct 2025 23:48:47 -0700 Subject: [PATCH 04/11] Initial plumbing complete --- livekit/sip.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/livekit/sip.go b/livekit/sip.go index 4a36c5e5..3ca560e8 100644 --- a/livekit/sip.go +++ b/livekit/sip.go @@ -11,6 +11,7 @@ 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" ) @@ -392,12 +393,12 @@ func (p *SIPInboundTrunkInfo) Validate() error { return err } if err := validateHeaders(p.Headers); err != nil { - fmt.Printf("Warning: Header validation failed for Headers field: %v\n", err) + logger.Warnw("Header validation failed for Headers field", err) // No error, just a warning for SIP RFC validation for now } // Don't bother with HeadersToAttributes. If they're invalid, we just won't match if err := validateHeaderNames(p.AttributesToHeaders); err != nil { - fmt.Printf("Warning: Header validation failed for AttributesToHeaders field: %v\n", err) + logger.Warnw("Header validation failed for AttributesToHeaders field", err) // No error, just a warning for SIP RFC validation for now } return nil @@ -488,12 +489,12 @@ func (p *SIPOutboundTrunkInfo) Validate() error { return err } if err := validateHeaders(p.Headers); err != nil { - fmt.Printf("Warning: Header validation failed for Headers field: %v\n", err) + logger.Warnw("Header validation failed for Headers field", err) // No error, just a warning for SIP RFC validation for now } // Don't bother with HeadersToAttributes. If they're invalid, we just won't match if err := validateHeaderNames(p.AttributesToHeaders); err != nil { - fmt.Printf("Warning: Header validation failed for AttributesToHeaders field: %v\n", err) + logger.Warnw("Header validation failed for AttributesToHeaders field", err) // No error, just a warning for SIP RFC validation for now } return nil @@ -515,7 +516,7 @@ func (p *SIPOutboundConfig) Validate() error { } // Don't bother with HeadersToAttributes. If they're invalid, we just won't match if err := validateHeaderNames(p.AttributesToHeaders); err != nil { - fmt.Printf("Warning: Header validation failed for AttributesToHeaders field: %v\n", err) + logger.Warnw("Header validation failed for AttributesToHeaders field", err) // No error, just a warning for SIP RFC validation for now } return nil @@ -724,7 +725,7 @@ func (p *CreateSIPParticipantRequest) Validate() error { } if err := validateHeaders(p.Headers); err != nil { - fmt.Printf("Warning: Header validation failed for Headers field: %v\n", err) + logger.Warnw("Header validation failed for Headers field", err) // No error, just a warning for SIP RFC validation for now } @@ -827,7 +828,7 @@ func (p *TransferSIPParticipantRequest) Validate() error { } if err := validateHeaders(p.Headers); err != nil { - fmt.Printf("Warning: Header validation failed for Headers field: %v\n", err) + logger.Warnw("Header validation failed for Headers field", err) // No error, just a warning for SIP RFC validation for now } From 2b12ec404d23b6ab38c9d0d9826458796fef9c91 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Oct 2025 00:04:01 -0700 Subject: [PATCH 05/11] Adding chageset --- .changeset/fair-beds-take.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fair-beds-take.md diff --git a/.changeset/fair-beds-take.md b/.changeset/fair-beds-take.md new file mode 100644 index 00000000..7336d7e9 --- /dev/null +++ b/.changeset/fair-beds-take.md @@ -0,0 +1,5 @@ +--- +"github.com/livekit/protocol": patch +--- + +Added warning prints to SIP headers From 31bb1f05a06d92bdb15d0550d0678099415dae6c Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Oct 2025 00:46:19 -0700 Subject: [PATCH 06/11] Tests correctly failing --- livekit/sip_validation_test.go | 126 +++++++++++++++------------------ 1 file changed, 59 insertions(+), 67 deletions(-) diff --git a/livekit/sip_validation_test.go b/livekit/sip_validation_test.go index de7075e3..d8a4df2e 100644 --- a/livekit/sip_validation_test.go +++ b/livekit/sip_validation_test.go @@ -79,22 +79,22 @@ 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{ - "alice@example.com", // basic email - "", // SIP URI with brackets - "Alice ", // display name + URI - "\"Alice Smith\" ", // quoted display name - "SIP/2.0/UDP 192.168.1.1:5060", // Via header - "1 INVITE", // CSeq header - "255", // Max-Forwards (max valid) - "0", // Max-Forwards (min valid) - "application/sdp", // Content-Type - "123", // Content-Length - "3600", // Expires - "call-123@example.com", // Call-ID - "text/plain; charset=utf-8", // Content-Type with params - "", // IPv6 URI - "\"Alice & Bob\" ", // display name with & symbol - strings.Repeat("a", 1024), // max lenth + "u1@example.com", // basic email + "", // SIP URI with brackets + "Alice ", // display name + URI + "\"Alice Smith\" ", // quoted display name + "SIP/2.0/UDP 192.168.1.1:5060", // Via header + "1 INVITE", // CSeq header + "255", // Max-Forwards (max valid) + "0", // Max-Forwards (min valid) + "application/sdp", // Content-Type + "123", // Content-Length + "3600", // Expires + "call-123@example.com", // Call-ID + "text/plain; charset=utf-8", // Content-Type with params + "", // IPv6 URI + "\"Alice & Bob\" ", // display name with & symbol + strings.Repeat("a", 1024), // max length } // Note: These restrictions are NOT in RFC 3261 but are applied for security/performance @@ -115,23 +115,6 @@ var InvalidHeaderValues = []string{ strings.Repeat("a", 1025), // too long } -// ValidDisplayNames contains valid display name formats -var ValidDisplayNames = []string{ - `"Alex Dev M" `, - `"Alex's \"Dev\" M\\" `, - `Alex Developer `, -} - -// InvalidDisplayNames contains invalid display name formats -var InvalidDisplayNames = []string{ - `"Alex "Dev" M" `, // unescaped quotes - `"Alex \M" `, // unescaped backslashes - `"Alex Developer" M `, // unmatched quotes - `Alex "Developer" M `, // unescaped quotes in unquoted - `"Alex Developer M `, // unterminated quote - `Alex Developer M" `, // unmatched quote -} - // testCaseName truncates a test case name to maxLen and adds dots with total size func testCaseName(name string, maxLen int, index int) string { if len(name) <= maxLen { @@ -144,44 +127,53 @@ func testCaseName(name string, maxLen int, index int) string { // ValidNameAddrHeaders contains valid Name-addr format headers with parameters var ValidNameAddrHeaders = []string{ - "sip:a1@example.com", // basic SIP URI (no brackets needed) - "sips:a2@example.com", // secure SIP URI (no brackets needed) - "tel:+1-555-123-4567", // TEL URI (no brackets needed) - "", // basic SIP URI with brackets - "", // secure SIP URI with brackets - "", // TEL URI with brackets - "Alice ", // display name + SIP URI - "\"Alice Smith\" ", // quoted display name - "", // SIP URI with transport - "", // SIP URI with flag param - "", // SIP URI with port - "", // SIP URI with multiple params - "Alice ", // display name + params - "\"Alice \\\"Quote\\\"\" ", // quoted display - "", // IPv6 with params - ";expires=60", // SIPS URI with expires parameter - "Alice ", // display name + params - "\"Alice & Bob\" ", // display name with & symbol + `"Alice Johnson" `, + `"Alice \"Ace\" Johnson's device\\" `, + `Alice Johnson `, + `sip:u4@example.com`, // basic SIP URI (no brackets needed) + `sips:u5@example.com`, // secure SIP URI (no brackets needed) + `tel:+1-555-123-4567`, // TEL URI (no brackets needed) + ``, // basic SIP URI with brackets + ``, // secure SIP URI with brackets + ``, // TEL URI with brackets + `Alice `, // display name + SIP URI + `"Alice Johnson" `, // quoted display name + ``, // SIP URI with transport + ``, // SIP URI with flag param + ``, // SIP URI with port + ``, // SIP URI with multiple params + `Alice `, // display name + params + `"Alice \"Ace\"" `, // quoted display + ``, // IPv6 with params + `;expires=60`, // SIPS URI with expires parameter + `Alice `, // display name + params + `"Alice & Bob" `, // display name with & symbol } // InvalidNameAddrHeaders contains invalid Name-addr format headers var InvalidNameAddrHeaders = []string{ - "", // missing opening bracket - " ", // multiple URIs - "Alice ", // multiple URIs with display - "Alice sip:u7@example.com", // display name without brackets - "Alice sips:u8@example.com", // display name without brackets - "Alice&Bob ", // display name with & symbol - "sip:u10@example.com;transport=tcp", // special chars without brackets - "sip:u11@example.com,transport=tcp", // comma without brackets - "sip:u12@example.com?transport=tcp", // question mark without brackets - "", // empty parameter value - "", // empty parameter name - "", // missing equals sign - "", // trailing semicolon - "", // double semicolon - "", // space in parameters + `"Alice "Ace" Johnson" `, // unescaped quotes + `"\Alice" `, // unescaped backslashes + `"Alice" Johnson `, // unmatched quotes + `Alice "Ace" Johnson `, // unescaped quotes in unquoted + `"Alice Johnson `, // unterminated quote + `Alice Johnson" `, // unmatched quote + ``, // missing opening bracket + ` `, // multiple URIs + `Alice `, // multiple URIs with display + `Alice sip:u13@example.com`, // display name without brackets + `Alice sips:u14@example.com`, // display name without brackets + `Alice & Bob `, // display name with & symbol + `sip:u16@example.com;transport=tcp`, // special chars without brackets + `sip:u17@example.com,transport=tcp`, // comma without brackets + `sip:u18@example.com?transport=tcp`, // question mark without brackets + ``, // empty parameter value + ``, // empty parameter name + ``, // missing equals sign + ``, // trailing semicolon + ``, // double semicolon + ``, // space in parameters } // TestValidateHeaderName_ValidHeaders tests that all valid header names pass validation From 249610c03dd48e014631dcf87ce3d812c6dc568a Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 6 Oct 2025 10:46:18 -0700 Subject: [PATCH 07/11] Fixed not using proper uquote and bare display name per spec --- livekit/sip_validation.go | 45 +++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/livekit/sip_validation.go b/livekit/sip_validation.go index 5c44e27f..dab4439a 100644 --- a/livekit/sip_validation.go +++ b/livekit/sip_validation.go @@ -18,11 +18,45 @@ import ( "errors" fmt "fmt" "regexp" + "strconv" "strings" ) // RFC 3261 compliant validation functions for SIP headers and messages +// validTokenCharacters is a lookup table for RFC 3261 Section 25.1 token characters +// token = 1*(alphanum / "-" / "." / "!" / "%" / "*" / "_" / "+" / "`" / "'" / "~") +// Also includes LWS characters (space, tab) for display-name validation +var validTokenCharacters [256]bool + +func init() { + // Initialize the lookup table for valid token characters + // Alphanumeric characters + for r := '0'; r <= '9'; r++ { + validTokenCharacters[r] = true + } + for r := 'A'; r <= 'Z'; r++ { + validTokenCharacters[r] = true + } + for r := 'a'; r <= 'z'; r++ { + validTokenCharacters[r] = true + } + + // RFC 3261 Section 25.1 - Token characters + LWS characters + validTokenCharacters['-'] = true + validTokenCharacters['.'] = true + validTokenCharacters['!'] = true + validTokenCharacters['%'] = true + validTokenCharacters['*'] = true + validTokenCharacters['_'] = true + validTokenCharacters['+'] = true + validTokenCharacters['`'] = true + validTokenCharacters['\''] = true + validTokenCharacters['~'] = true + validTokenCharacters[' '] = true + validTokenCharacters['\t'] = true +} + // RFC 3261 Section 25.1 - Header field names // token = 1*(alphanum / "-" / "." / "!" / "%" / "*" / "_" / "+" / "`" / "'" / "~") // Specifically lowercase since we're converting to lowercase for case-insensitive comparison @@ -229,18 +263,17 @@ func validateDisplayName(displayName string) error { // Check if display name is quoted if strings.HasPrefix(displayName, "\"") && strings.HasSuffix(displayName, "\"") { - // Quoted display name - basic validation - quoted := displayName[1 : len(displayName)-1] - // Check for proper escaping - if strings.Contains(quoted, "\"") && !strings.Contains(quoted, "\\\"") { - return errors.New("unquoted display name contains unescaped quotes") + // Quoted display name - use strconv.Unquote to validate proper escaping + _, err := strconv.Unquote(displayName) + if err != nil { + return fmt.Errorf("quoted display name contains invalid escape sequences: %v", err) } return nil } // Unquoted display name - must not contain special characters for _, r := range displayName { - if r == '<' || r == '>' || r == '"' || r == '\\' || r == '&' { + if int(r) >= len(validTokenCharacters) || !validTokenCharacters[r] { return errors.New("unquoted display name contains special characters") } } From 9212f244df69b0f23882977a57ab038ccd73339b Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 9 Oct 2025 16:42:03 -0700 Subject: [PATCH 08/11] PR comments, using Unquote, removing heavy name-addr processing and just do basic URI parse --- livekit/sip_validation.go | 364 ++++++++++++++------------------- livekit/sip_validation_test.go | 4 - 2 files changed, 156 insertions(+), 212 deletions(-) diff --git a/livekit/sip_validation.go b/livekit/sip_validation.go index dab4439a..6929ab2e 100644 --- a/livekit/sip_validation.go +++ b/livekit/sip_validation.go @@ -24,37 +24,125 @@ import ( // RFC 3261 compliant validation functions for SIP headers and messages -// validTokenCharacters is a lookup table for RFC 3261 Section 25.1 token characters -// token = 1*(alphanum / "-" / "." / "!" / "%" / "*" / "_" / "+" / "`" / "'" / "~") -// Also includes LWS characters (space, tab) for display-name validation -var validTokenCharacters [256]bool +type AllowedCharacters struct { + ascii [127]bool + utf8 bool +} -func init() { - // Initialize the lookup table for valid token characters - // Alphanumeric characters +func NewAllowedCharacters() *AllowedCharacters { + return &AllowedCharacters{} +} + +func (a *AllowedCharacters) AddUTF8() error { + a.utf8 = true + return nil +} + +func (a *AllowedCharacters) AddNumbers() error { for r := '0'; r <= '9'; r++ { - validTokenCharacters[r] = true + a.ascii[r] = true + } + return nil +} + +func (a *AllowedCharacters) AddLowercaseASCII() error { + for r := 'a'; r <= 'z'; r++ { + a.ascii[r] = true } + return nil +} + +func (a *AllowedCharacters) AddUppercaseASCII() error { for r := 'A'; r <= 'Z'; r++ { - validTokenCharacters[r] = true + a.ascii[r] = true } - for r := 'a'; r <= 'z'; r++ { - validTokenCharacters[r] = true + return nil +} + +func (a *AllowedCharacters) AddPrintableLienarASCII() { + // Anything between 0x20 and 0x7E + for i := 0x20; i <= 0x7E; i++ { + a.ascii[i] = true + } +} + +func (a *AllowedCharacters) Add(chars string) error { + for _, char := range chars { + if int(char) >= len(a.ascii) { + return fmt.Errorf("char %d out of range, consider explicilty adding utf8 characters", char) + } + a.ascii[char] = true + } + return nil +} + +func (a *AllowedCharacters) Remove(chars string) error { + for _, char := range chars { + if int(char) >= len(a.ascii) { + return fmt.Errorf("char %d out of range, consider explicilty adding utf8 characters", char) + } + a.ascii[char] = false } + return nil +} - // RFC 3261 Section 25.1 - Token characters + LWS characters - validTokenCharacters['-'] = true - validTokenCharacters['.'] = true - validTokenCharacters['!'] = true - validTokenCharacters['%'] = true - validTokenCharacters['*'] = true - validTokenCharacters['_'] = true - validTokenCharacters['+'] = true - validTokenCharacters['`'] = true - validTokenCharacters['\''] = true - validTokenCharacters['~'] = true - validTokenCharacters[' '] = true - validTokenCharacters['\t'] = true +func (a *AllowedCharacters) Copy() *AllowedCharacters { + return &AllowedCharacters{ + ascii: a.ascii, + utf8: a.utf8, + } +} + +func (a *AllowedCharacters) Validate(target string) error { + for _, char := range target { + if int(char) >= len(a.ascii) && !a.utf8 { + return fmt.Errorf("char %d out of range, consider explicilty adding utf8 characters", char) + } + if !a.ascii[char] { + return fmt.Errorf("char %d not allowed", char) + } + } + return nil +} + +var tokenCharacters *AllowedCharacters +var displayNameCharacters *AllowedCharacters +var headerValuesCharacters *AllowedCharacters + +func init() { + // Per RFC 3261 Section 25.1 + // SIP-message = Request / Response + // Request = Request-Line *( message-header ) CRLF [ message-body ] + // Response = Status-Line *( message-header ) CRLF [ message-body ] + // Request-Line = Method SP Request-URI SP SIP-Version CRLF + // Method = (CAPITAL ASCII) + // Request-URI = SIP-URI / SIPS-URI / absoluteURI + // SIP-Version = "SIP" "/" 1*DIGIT "." 1*DIGIT (CAPITAL ASCII, DIGITS, "/.") + // Status-Line = SIP-Version SP Status-Code SP Reason-Phrase CRLF + // Status-Code = (Alphanum + "-") + // Reason-Phrase = (Basically whatever...) + // extension-header = header-name (token) ":" header-value (Basically whatever...) + + // URIs + // SIP-URI = "sip:" [ userinfo ] hostport uri-parameters [ headers ] + // SIPS-URI = "sips:" [ userinfo ] hostport uri-parameters [ headers ] + + // One specific header form we care about: + // name-addr = [ display-name ] LAQUOT addr-spec RAQUOT + // display-name = *(token LWS)/ quoted-string + // addr-spec = SIP-URI / SIPS-URI / absoluteURI + + tokenCharacters = NewAllowedCharacters() + tokenCharacters.AddNumbers() + tokenCharacters.AddLowercaseASCII() + tokenCharacters.AddUppercaseASCII() + tokenCharacters.Add("-.!%*_+`'~") + + displayNameCharacters = tokenCharacters.Copy() + displayNameCharacters.Add(" \t") + + headerValuesCharacters = NewAllowedCharacters() + headerValuesCharacters.AddPrintableLienarASCII() // Specifically not adding UTF8 for now } // RFC 3261 Section 25.1 - Header field names @@ -107,27 +195,16 @@ var FrobiddenSipHeaderNames = map[string]bool{ "max-forwards": true, "record-route": true, "refer-to": true, // rfc3515 - "referred-by": true, // rfc3892 + "referred-by": true, // rfc3892sipUriCharacters "reply-to": true, - "route": true, - "supported": true, - "to": true, // We might allow this in the future, but for now we're printing - "via": true, - - // Single-letter shorthands, a.k.a compact form - "b": true, // Referred-By; rfc3892 - "c": true, // Content-Type - "e": true, // Content-Encoding - "f": true, // From - "i": true, // Call-ID - "k": true, // Supported - "l": true, // Content-Length - "m": true, // Contact - "o": true, // Event; rfc3903 - "r": true, // Refer-To; rfc3515 - "t": true, // To - "u": true, // Allow-Events; rfc3903 - "v": true, // Via + "k": true, // Supported + "l": true, // Content-Length + "m": true, // Contact + "o": true, // Event; rfc3903 + "r": true, // Refer-To; rfc3515 + "t": true, // To + "u": true, // Allow-Events; rfc3903 + "v": true, // Via } // Headers that must comply with name-addr specification per RFC 3261 Section 20.10 @@ -153,6 +230,10 @@ func ValidateHeaderName(name string) error { return errors.New("header name too long (max 255 characters)") } + if err := tokenCharacters.Validate(name); err != nil { + return fmt.Errorf("header name %s contains invalid characters: %w", name, err) + } + lowerName := strings.ToLower(name) if !reHeaderName.MatchString(lowerName) { return errors.New("header name contains invalid characters") @@ -169,23 +250,25 @@ 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 errors.New("header value cannot be empty") + return fmt.Errorf("header %s: value cannot be empty", name) } if len(value) > 1024 { - return errors.New("header value too long (max 1024 characters)") + return fmt.Errorf("header %s: value too long (max 1024 characters)", name) } - // Basic character validation - printable ASCII - if !reHeaderValueBasic.MatchString(value) { - return errors.New("header value contains invalid characters") + // Basic character validation - printable ASCII. We're stricter than the spec here - no UTF-8 for now + if err := headerValuesCharacters.Validate(value); err != nil { + return fmt.Errorf("header %s: value: %w", name, err) } // Convert to lowercase for case-insensitive comparison lowerName := strings.ToLower(name) if _, exists := nameAddrHeaders[lowerName]; exists && false { // TODO: Disabled since all supported headers are forbidden, re-enable when we allow some - return validateNameAddrHeader(value) + if err := validateNameAddrHeader(value); err != nil { + return fmt.Errorf("header %s: value: %w", name, err) + } } return nil @@ -231,28 +314,23 @@ func validateNameAddrHeader(value string) error { // name-addr = [display-name] // addr-spec = SIP-URI / SIPS-URI / absoluteURI + uri := value start, end, err := findAngleBrackets(value) if err != nil { return err } if start >= 0 || end >= 0 { - displayName := strings.TrimSpace(value[:start]) - uri := value[start+1 : end] - if displayName != "" { - if err := validateDisplayName(displayName); err != nil { - return err - } + uri = value[start+1 : end] + if err := validateDisplayName(strings.TrimSpace(value[:start])); err != nil { + return err + } + } else { + // This is a bare URI, and should comply with addr-spec, no special characters + if strings.ContainsAny(value, ";,? ") { + return errors.New("bare URI with special characters") } - // Keep in mind, this ignores header parameters, while validating URI parameters - return validateURI(uri) - } - - // This is a bare URI, and should comply with addr-spec, no special characters - if strings.ContainsAny(value, ";,? ") { - return errors.New("bare URI with special characters") } - - return validateURI(value) + return validateURI(uri) } // validateDisplayName validates a display name in name-addr format @@ -262,20 +340,18 @@ func validateDisplayName(displayName string) error { } // Check if display name is quoted - if strings.HasPrefix(displayName, "\"") && strings.HasSuffix(displayName, "\"") { + if strings.HasPrefix(displayName, `"`) && strings.HasSuffix(displayName, `"`) { // Quoted display name - use strconv.Unquote to validate proper escaping _, err := strconv.Unquote(displayName) if err != nil { - return fmt.Errorf("quoted display name contains invalid escape sequences: %v", err) + return fmt.Errorf("display name: %w", err) } return nil } // Unquoted display name - must not contain special characters - for _, r := range displayName { - if int(r) >= len(validTokenCharacters) || !validTokenCharacters[r] { - return errors.New("unquoted display name contains special characters") - } + if err := displayNameCharacters.Validate(displayName); err != nil { + return fmt.Errorf("display name: %w", err) } return nil @@ -283,146 +359,18 @@ func validateDisplayName(displayName string) error { // validateURI validates URIs that can appear in name-addr format func validateURI(uri string) error { - if uri == "" { - return errors.New("URI cannot be empty") - } - - // Check for SIP/SIPS scheme - if strings.HasPrefix(uri, "sip:") || strings.HasPrefix(uri, "sips:") { - return validateSIPURI(uri) - } - - // Check for TEL scheme - if strings.HasPrefix(uri, "tel:") { - return validateTELURI(uri) - } - - // For now, only support SIP/SIPS and TEL URIs - // RFC 3261 allows other absolute URIs, but we'll be restrictive - return errors.New("URI scheme must match one of sip, sips, or tel") -} - -// validateSIPURI validates a SIP or SIPS URI per RFC 3261 Section 19.1 -func validateSIPURI(uri string) error { - if uri == "" { - return errors.New("SIP URI cannot be empty") - } - - // Check for SIP or SIPS scheme - if !strings.HasPrefix(uri, "sip:") && !strings.HasPrefix(uri, "sips:") { - return errors.New("SIP URI scheme must match sip or sips") - } - - // Basic format validation - if !reSIPURI.MatchString(uri) { - return errors.New("SIP URI format is invalid") - } - - // Validate URI parameters if present - if strings.Contains(uri, ";") { - return validateURIParameters(uri) - } - - return nil -} - -// validateURIParameters validates URI parameters -func validateURIParameters(uri string) error { - // Split URI into base and parameters - parts := strings.Split(uri, ";") - if len(parts) < 2 { - return errors.New("invalid URI parameters") - } - - // Validate each parameter - for _, param := range parts[1:] { - param = strings.TrimSpace(param) - if param == "" { - return errors.New("empty URI parameter") - } - - // Check for valid parameter format: name=value or name - if strings.Contains(param, "=") { - paramParts := strings.SplitN(param, "=", 2) - if len(paramParts) != 2 { - return errors.New("invalid URI parameter format") - } - name := strings.TrimSpace(paramParts[0]) - value := strings.TrimSpace(paramParts[1]) - - if name == "" { - return errors.New("URI parameter name cannot be empty") - } - if value == "" { - return errors.New("URI parameter value cannot be empty") - } - - } else { - // Parameter without value - just validate name - if strings.TrimSpace(param) == "" { - return errors.New("URI parameter name cannot be empty") - } - } - - // Check for invalid characters in parameter - if strings.Contains(param, " ") { - return errors.New("URI parameter contains spaces") - } - } - - return nil -} - -// validateTELURI validates a TEL URI per RFC 3966 -func validateTELURI(uri string) error { - if uri == "" { - return errors.New("TEL URI cannot be empty") + // Just do the basics, full validation should be done by sip service + scheme := strings.SplitN(uri, ":", 2)[0] + if scheme != "sip" && scheme != "sips" && scheme != "tel" { + // Technically, it either needs to be sip/s: or scheme://... + // Thus, tel: uri should not be supported here... but we allow it because of de-facto usage. + return errors.New("uri: scheme not one of sip, sips, or tel") } - if !strings.HasPrefix(uri, "tel:") { - return errors.New("TEL URI scheme must match tel") - } - - // Basic validation - TEL URIs are more complex, this is simplified - if len(uri) < 5 { // "tel:" + at least one character - return errors.New("TEL URI format is invalid") + // Just no spaces, proper validation should be done in sip service + if strings.Contains(uri, " ") { + return errors.New("uri: contains spaces") } return nil } - -// EscapeHeaderValue escapes special characters in header values per RFC 3261 -func EscapeHeaderValue(value string) string { - // Escape special characters that need to be quoted - var result strings.Builder - for _, r := range value { - switch r { - case ' ', '\t', '\r', '\n', '"', '\\': - // These characters need to be escaped or quoted - result.WriteString("\\") - result.WriteRune(r) - default: - result.WriteRune(r) - } - } - return result.String() -} - -// UnescapeHeaderValue removes escaping from header values -func UnescapeHeaderValue(value string) string { - var result strings.Builder - escaped := false - - for _, r := range value { - if escaped { - result.WriteRune(r) - escaped = false - } else if r == '\\' { - escaped = true - } else { - result.WriteRune(r) - } - } - - return result.String() -} diff --git a/livekit/sip_validation_test.go b/livekit/sip_validation_test.go index d8a4df2e..9c9be283 100644 --- a/livekit/sip_validation_test.go +++ b/livekit/sip_validation_test.go @@ -168,11 +168,7 @@ var InvalidNameAddrHeaders = []string{ `sip:u16@example.com;transport=tcp`, // special chars without brackets `sip:u17@example.com,transport=tcp`, // comma without brackets `sip:u18@example.com?transport=tcp`, // question mark without brackets - ``, // empty parameter value - ``, // empty parameter name ``, // missing equals sign - ``, // trailing semicolon - ``, // double semicolon ``, // space in parameters } From 1c50813a277de6a40dffae142174f82ca02b4031 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 9 Oct 2025 16:45:03 -0700 Subject: [PATCH 09/11] Dropping now-unused regex --- livekit/sip_validation.go | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/livekit/sip_validation.go b/livekit/sip_validation.go index 6929ab2e..30f09329 100644 --- a/livekit/sip_validation.go +++ b/livekit/sip_validation.go @@ -17,7 +17,6 @@ package livekit import ( "errors" fmt "fmt" - "regexp" "strconv" "strings" ) @@ -145,18 +144,6 @@ func init() { headerValuesCharacters.AddPrintableLienarASCII() // Specifically not adding UTF8 for now } -// RFC 3261 Section 25.1 - Header field names -// token = 1*(alphanum / "-" / "." / "!" / "%" / "*" / "_" / "+" / "`" / "'" / "~") -// Specifically lowercase since we're converting to lowercase for case-insensitive comparison -var reHeaderName = regexp.MustCompile(`^[a-z0-9\-\.!%*_+` + "`" + `'~]+$`) - -// RFC 3261 Section 25.1 - Header field values (basic validation) -// More specific validation is done per header type -var reHeaderValueBasic = regexp.MustCompile(`^[\x20-\x7E]*$`) - -// RFC 3261 Section 19.1 - SIP URI validation -var reSIPURI = regexp.MustCompile(`^(sip|sips):([^@]+@)?([^;]+)(;.*)?$`) - // Required headers for SIP requests per RFC 3261 Section 8.1.1 var RequiredRequestHeaders = map[string]bool{ "via": true, @@ -234,12 +221,8 @@ func ValidateHeaderName(name string) error { return fmt.Errorf("header name %s contains invalid characters: %w", name, err) } - lowerName := strings.ToLower(name) - if !reHeaderName.MatchString(lowerName) { - return errors.New("header name contains invalid characters") - } - // 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) } From b6bd036e83545d6e2202de88514d34c3d470ce2a Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 14 Oct 2025 11:53:52 -0700 Subject: [PATCH 10/11] Added todo --- livekit/sip.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/livekit/sip.go b/livekit/sip.go index 3ca560e8..2832e838 100644 --- a/livekit/sip.go +++ b/livekit/sip.go @@ -394,12 +394,12 @@ func (p *SIPInboundTrunkInfo) Validate() error { } if err := validateHeaders(p.Headers); err != nil { logger.Warnw("Header validation failed for Headers field", err) - // No error, just a warning for SIP RFC validation for now + // 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) - // No error, just a warning for SIP RFC validation for now + // TODO: Once we're happy with the validation, we want this to error out } return nil } @@ -490,12 +490,12 @@ func (p *SIPOutboundTrunkInfo) Validate() error { } if err := validateHeaders(p.Headers); err != nil { logger.Warnw("Header validation failed for Headers field", err) - // No error, just a warning for SIP RFC validation for now + // 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) - // No error, just a warning for SIP RFC validation for now + // TODO: Once we're happy with the validation, we want this to error out } return nil } @@ -726,7 +726,7 @@ func (p *CreateSIPParticipantRequest) Validate() error { if err := validateHeaders(p.Headers); err != nil { logger.Warnw("Header validation failed for Headers field", err) - // No error, just a warning for SIP RFC validation for now + // TODO: Once we're happy with the validation, we want this to error out } // Validate display_name if provided @@ -735,7 +735,7 @@ func (p *CreateSIPParticipantRequest) Validate() error { return errors.New("display_name too long (max 128 characters)") } - // TODO: Validate display name doesn't contain invalid characters + // TODO: Once we're happy with the validation, we want this to error out } // Validate destination if provided @@ -829,7 +829,7 @@ func (p *TransferSIPParticipantRequest) Validate() error { if err := validateHeaders(p.Headers); err != nil { logger.Warnw("Header validation failed for Headers field", err) - // No error, just a warning for SIP RFC validation for now + // TODO: Once we're happy with the validation, we want this to error out } return nil From b4e3651fa5265d49e63ff6a1735ee0d8472ef55d Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 22 Oct 2025 17:16:12 -0700 Subject: [PATCH 11/11] pr suggestion --- livekit/sip_validation.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/livekit/sip_validation.go b/livekit/sip_validation.go index 30f09329..56386e7a 100644 --- a/livekit/sip_validation.go +++ b/livekit/sip_validation.go @@ -23,49 +23,49 @@ import ( // RFC 3261 compliant validation functions for SIP headers and messages -type AllowedCharacters struct { +type allowedCharacters struct { ascii [127]bool utf8 bool } -func NewAllowedCharacters() *AllowedCharacters { - return &AllowedCharacters{} +func NewAllowedCharacters() *allowedCharacters { + return &allowedCharacters{} } -func (a *AllowedCharacters) AddUTF8() error { +func (a *allowedCharacters) AddUTF8() error { a.utf8 = true return nil } -func (a *AllowedCharacters) AddNumbers() error { +func (a *allowedCharacters) AddNumbers() error { for r := '0'; r <= '9'; r++ { a.ascii[r] = true } return nil } -func (a *AllowedCharacters) AddLowercaseASCII() error { +func (a *allowedCharacters) AddLowercaseASCII() error { for r := 'a'; r <= 'z'; r++ { a.ascii[r] = true } return nil } -func (a *AllowedCharacters) AddUppercaseASCII() error { +func (a *allowedCharacters) AddUppercaseASCII() error { for r := 'A'; r <= 'Z'; r++ { a.ascii[r] = true } return nil } -func (a *AllowedCharacters) AddPrintableLienarASCII() { +func (a *allowedCharacters) AddPrintableLienarASCII() { // Anything between 0x20 and 0x7E for i := 0x20; i <= 0x7E; i++ { a.ascii[i] = true } } -func (a *AllowedCharacters) Add(chars string) error { +func (a *allowedCharacters) Add(chars string) error { for _, char := range chars { if int(char) >= len(a.ascii) { return fmt.Errorf("char %d out of range, consider explicilty adding utf8 characters", char) @@ -75,7 +75,7 @@ func (a *AllowedCharacters) Add(chars string) error { return nil } -func (a *AllowedCharacters) Remove(chars string) error { +func (a *allowedCharacters) Remove(chars string) error { for _, char := range chars { if int(char) >= len(a.ascii) { return fmt.Errorf("char %d out of range, consider explicilty adding utf8 characters", char) @@ -85,14 +85,14 @@ func (a *AllowedCharacters) Remove(chars string) error { return nil } -func (a *AllowedCharacters) Copy() *AllowedCharacters { - return &AllowedCharacters{ +func (a *allowedCharacters) Copy() *allowedCharacters { + return &allowedCharacters{ ascii: a.ascii, utf8: a.utf8, } } -func (a *AllowedCharacters) Validate(target string) error { +func (a *allowedCharacters) Validate(target string) error { for _, char := range target { if int(char) >= len(a.ascii) && !a.utf8 { return fmt.Errorf("char %d out of range, consider explicilty adding utf8 characters", char) @@ -104,9 +104,9 @@ func (a *AllowedCharacters) Validate(target string) error { return nil } -var tokenCharacters *AllowedCharacters -var displayNameCharacters *AllowedCharacters -var headerValuesCharacters *AllowedCharacters +var tokenCharacters *allowedCharacters +var displayNameCharacters *allowedCharacters +var headerValuesCharacters *allowedCharacters func init() { // Per RFC 3261 Section 25.1