From 0e6426c648e6007d2a3526244431acedeb382940 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 19 Jul 2026 10:20:37 +0200 Subject: [PATCH] adjust code to prevent security scan false positives (#5963) ... about string escaping. --- internal/protocols/whip/link_header.go | 117 ++++++++++++++++---- internal/protocols/whip/link_header_test.go | 41 +++++++ 2 files changed, 135 insertions(+), 23 deletions(-) diff --git a/internal/protocols/whip/link_header.go b/internal/protocols/whip/link_header.go index 93232ea4..0f862847 100644 --- a/internal/protocols/whip/link_header.go +++ b/internal/protocols/whip/link_header.go @@ -1,23 +1,58 @@ package whip import ( - "encoding/json" "fmt" - "regexp" + "strings" "github.com/pion/webrtc/v4" ) +var linkHeaderCredentialReplacer = strings.NewReplacer( + `\`, `\\`, + `"`, `\"`, +) + func quoteCredential(v string) string { - b, _ := json.Marshal(v) - s := string(b) - return s[1 : len(s)-1] + return linkHeaderCredentialReplacer.Replace(v) } -func unquoteCredential(v string) string { - var s string - json.Unmarshal([]byte("\""+v+"\""), &s) //nolint:errcheck - return s +func readQuotedCredential(v string) (string, string, bool) { + if len(v) == 0 || v[0] != '"' { + return "", "", false + } + + var ret strings.Builder + ret.Grow(len(v) - 1) + + escaped := false + + for i := 1; i < len(v); i++ { + switch v[i] { + case '\\': + if escaped { + ret.WriteByte('\\') + escaped = false + } else { + escaped = true + } + + case '"': + if escaped { + ret.WriteByte('"') + escaped = false + } else { + return ret.String(), v[i+1:], true + } + + default: + if escaped { + return "", "", false + } + ret.WriteByte(v[i]) + } + } + + return "", "", false } // LinkHeaderMarshal encodes a link header. @@ -25,37 +60,73 @@ func LinkHeaderMarshal(iceServers []webrtc.ICEServer) []string { ret := make([]string, len(iceServers)) for i, server := range iceServers { - link := "<" + server.URLs[0] + ">; rel=\"ice-server\"" + var link strings.Builder + + link.WriteByte('<') + link.WriteString(server.URLs[0]) + link.WriteString(`>; rel="ice-server"`) + if server.Username != "" { - link += "; username=\"" + quoteCredential(server.Username) + "\"" + - "; credential=\"" + quoteCredential(server.Credential.(string)) + "\"; credential-type=\"password\"" + link.WriteString(`; username="`) + link.WriteString(quoteCredential(server.Username)) + link.WriteString(`"; credential="`) + link.WriteString(quoteCredential(server.Credential.(string))) + link.WriteString(`"; credential-type="password"`) } - ret[i] = link + + ret[i] = link.String() } return ret } -var reLink = regexp.MustCompile(`^<(.+?)>; rel="ice-server"(; username="(.+?)"` + - `; credential="(.+?)"; credential-type="password")?`) - // LinkHeaderUnmarshal decodes a link header. func LinkHeaderUnmarshal(link []string) ([]webrtc.ICEServer, error) { ret := make([]webrtc.ICEServer, len(link)) for i, li := range link { - m := reLink.FindStringSubmatch(li) - if m == nil { + var ok bool + li, ok = strings.CutPrefix(li, "<") + if !ok { return nil, fmt.Errorf("invalid link header: '%s'", li) } - s := webrtc.ICEServer{ - URLs: []string{m[1]}, + var url string + url, li, ok = strings.Cut(li, `>; rel="ice-server"`) + if !ok { + return nil, fmt.Errorf("invalid link header: '<%s'", li) } - if m[3] != "" { - s.Username = unquoteCredential(m[3]) - s.Credential = unquoteCredential(m[4]) + s := webrtc.ICEServer{ + URLs: []string{url}, + } + + if li != "" { + li, ok = strings.CutPrefix(li, `; username=`) + if !ok { + return nil, fmt.Errorf("invalid link header: '<%s'", li) + } + + s.Username, li, ok = readQuotedCredential(li) + if !ok || s.Username == "" { + return nil, fmt.Errorf("invalid link header: '<%s'", li) + } + + li, ok = strings.CutPrefix(li, `; credential=`) + if !ok { + return nil, fmt.Errorf("invalid link header: '<%s'", li) + } + + s.Credential, li, ok = readQuotedCredential(li) + if !ok { + return nil, fmt.Errorf("invalid link header: '<%s'", li) + } + + li, ok = strings.CutPrefix(li, `; credential-type="password"`) + if !ok || li != "" { + return nil, fmt.Errorf("invalid link header: '<%s'", li) + } + s.CredentialType = webrtc.ICECredentialTypePassword } diff --git a/internal/protocols/whip/link_header_test.go b/internal/protocols/whip/link_header_test.go index 1a432524..0f797a43 100644 --- a/internal/protocols/whip/link_header_test.go +++ b/internal/protocols/whip/link_header_test.go @@ -30,6 +30,20 @@ var linkHeaderCases = []struct { }, }, }, + { + "slashes-and-quotes", + []string{ + `; rel="ice-server"; username="my\\user\"a"; ` + + `credential="my\\pwd\"b"; credential-type="password"`, + }, + []webrtc.ICEServer{ + { + URLs: []string{"turns:turn.example.com"}, + Username: "my\\user\"a", + Credential: "my\\pwd\"b", + }, + }, + }, } func TestLinkHeaderUnmarshal(t *testing.T) { @@ -50,3 +64,30 @@ func TestLinkHeaderMarshal(t *testing.T) { }) } } + +func TestLinkHeaderUnmarshalInvalid(t *testing.T) { + for _, ca := range []struct { + name string + enc []string + }{ + { + "invalid escape in username", + []string{ + `; rel="ice-server"; username="my\nuser"; ` + + `credential="mypwd"; credential-type="password"`, + }, + }, + { + "truncated escape in credential", + []string{ + `; rel="ice-server"; username="myuser"; ` + + `credential="mypwd\"; credential-type="password"`, + }, + }, + } { + t.Run(ca.name, func(t *testing.T) { + _, err := LinkHeaderUnmarshal(ca.enc) + require.Error(t, err) + }) + } +}