From 4c5f558006c93a4e6eb4ef0ca558b8932c92b67a Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Thu, 23 Jul 2026 23:06:15 +0200 Subject: [PATCH] hls, webrtc: prevent cross-origin unauthorized access (#5975) when a user had previously inserted credentials into a MediaMTX instance through a browser, and AllowOrigins was set to a wildcard, third-party websites visited by the user were allowed to read streams without restrictions. This is now prevented by returning "*" in Access-Control-Allow-Origins when AllowOrigins is a wildcard, a behavior that prevents browsers from sharing credentials with third-party websites. --- docs/4-read/07-web-browsers.md | 2 - internal/api/api_test.go | 1 - internal/metrics/metrics_test.go | 1 - internal/playback/server_test.go | 1 - internal/pprof/pprof_test.go | 1 - internal/protocols/httpp/handler_origin.go | 89 +++++++++---------- .../protocols/httpp/handler_origin_test.go | 8 +- internal/servers/hls/server_test.go | 1 - internal/servers/webrtc/server_test.go | 1 - 9 files changed, 49 insertions(+), 56 deletions(-) diff --git a/docs/4-read/07-web-browsers.md b/docs/4-read/07-web-browsers.md index 9871a1d8..2aa68bda 100644 --- a/docs/4-read/07-web-browsers.md +++ b/docs/4-read/07-web-browsers.md @@ -179,8 +179,6 @@ After the video tag, add a script that initializes the stream when the page is f if (Hls.isSupported()) { const hls = new Hls({ xhrSetup: function (xhr, url) { - xhr.withCredentials = true; - let user = ""; // fill if needed let pass = ""; // fill if needed let token = ""; // fill if needed diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 9524b237..c97ad7b4 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -116,7 +116,6 @@ func TestPreflightRequest(t *testing.T) { require.NoError(t, err) require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) - require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) require.Equal(t, "OPTIONS, GET, POST, PATCH, DELETE", res.Header.Get("Access-Control-Allow-Methods")) require.Equal(t, "Authorization, Content-Type", res.Header.Get("Access-Control-Allow-Headers")) require.Equal(t, byts, []byte{}) diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go index f766c380..7316e1df 100644 --- a/internal/metrics/metrics_test.go +++ b/internal/metrics/metrics_test.go @@ -482,7 +482,6 @@ func TestPreflightRequest(t *testing.T) { require.NoError(t, err) require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) - require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods")) require.Equal(t, "Authorization", res.Header.Get("Access-Control-Allow-Headers")) require.Equal(t, byts, []byte{}) diff --git a/internal/playback/server_test.go b/internal/playback/server_test.go index 1a027ecf..cfbbefea 100644 --- a/internal/playback/server_test.go +++ b/internal/playback/server_test.go @@ -45,7 +45,6 @@ func TestPreflightRequest(t *testing.T) { require.NoError(t, err) require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) - require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods")) require.Equal(t, "Authorization", res.Header.Get("Access-Control-Allow-Headers")) require.Equal(t, byts, []byte{}) diff --git a/internal/pprof/pprof_test.go b/internal/pprof/pprof_test.go index 6bd92599..cb9850ba 100644 --- a/internal/pprof/pprof_test.go +++ b/internal/pprof/pprof_test.go @@ -44,7 +44,6 @@ func TestPreflightRequest(t *testing.T) { require.NoError(t, err) require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) - require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods")) require.Equal(t, "Authorization", res.Header.Get("Access-Control-Allow-Headers")) require.Equal(t, byts, []byte{}) diff --git a/internal/protocols/httpp/handler_origin.go b/internal/protocols/httpp/handler_origin.go index 1f7bbc1c..41fce9ec 100644 --- a/internal/protocols/httpp/handler_origin.go +++ b/internal/protocols/httpp/handler_origin.go @@ -5,6 +5,7 @@ import ( "net/http" "net/url" "regexp" + "slices" "strings" ) @@ -13,68 +14,63 @@ func isOriginAllowed(origin string, allowOrigins []string) (string, bool) { return "", false } - for _, o := range allowOrigins { - if o == "*" { - if origin != "" { - return origin, true - } - return "*", true - } - } - - if origin == "" { - return "", false - } - - originURL, err := url.Parse(origin) - if err != nil || originURL.Scheme == "" { - return "", false - } - - if originURL.Port() == "" && originURL.Scheme != "" { - switch originURL.Scheme { - case "http": - originURL.Host = net.JoinHostPort(originURL.Host, "80") - case "https": - originURL.Host = net.JoinHostPort(originURL.Host, "443") - } - } - - for _, o := range allowOrigins { - allowedURL, errAllowed := url.Parse(o) - if errAllowed != nil { - continue + if origin != "" { + originURL, err := url.Parse(origin) + if err != nil || originURL.Scheme == "" { + return "", false } - if allowedURL.Port() == "" { - switch allowedURL.Scheme { + if originURL.Port() == "" && originURL.Scheme != "" { + switch originURL.Scheme { case "http": - allowedURL.Host = net.JoinHostPort(allowedURL.Host, "80") + originURL.Host = net.JoinHostPort(originURL.Host, "80") case "https": - allowedURL.Host = net.JoinHostPort(allowedURL.Host, "443") + originURL.Host = net.JoinHostPort(originURL.Host, "443") } } - if allowedURL.Scheme == originURL.Scheme && - allowedURL.Host == originURL.Host && - allowedURL.Port() == originURL.Port() { - return origin, true - } + for _, o := range allowOrigins { + allowedURL, errAllowed := url.Parse(o) + if errAllowed != nil { + continue + } - if strings.Contains(allowedURL.Host, "*") { - pattern := strings.ReplaceAll(allowedURL.Host, "*.", "(.*\\.)?") - pattern = strings.ReplaceAll(pattern, "*", ".*") - matched, errMatched := regexp.MatchString("^"+pattern+"$", originURL.Host) - if errMatched == nil && matched { + if allowedURL.Port() == "" { + switch allowedURL.Scheme { + case "http": + allowedURL.Host = net.JoinHostPort(allowedURL.Host, "80") + case "https": + allowedURL.Host = net.JoinHostPort(allowedURL.Host, "443") + } + } + + if allowedURL.Scheme == originURL.Scheme && + allowedURL.Host == originURL.Host && + allowedURL.Port() == originURL.Port() { return origin, true } + + if strings.Contains(allowedURL.Host, "*") { + pattern := strings.ReplaceAll(allowedURL.Host, "*.", "(.*\\.)?") + pattern = strings.ReplaceAll(pattern, "*", ".*") + matched, errMatched := regexp.MatchString("^"+pattern+"$", originURL.Host) + if errMatched == nil && matched { + return origin, true + } + } } } + // return wildcard as last resort only + // because it blocks cross-origin requests with cookies + if slices.Contains(allowOrigins, "*") { + return "*", true + } + return "", false } -// add Access-Control-Allow-Origin and Access-Control-Allow-Credentials headers. +// add Access-Control-Allow-Origin header. type handlerOrigin struct { h http.Handler allowOrigins []string @@ -84,7 +80,6 @@ func (h *handlerOrigin) ServeHTTP(w http.ResponseWriter, r *http.Request) { origin, ok := isOriginAllowed(r.Header.Get("Origin"), h.allowOrigins) if ok { w.Header().Set("Access-Control-Allow-Origin", origin) - w.Header().Set("Access-Control-Allow-Credentials", "true") } h.h.ServeHTTP(w, r) diff --git a/internal/protocols/httpp/handler_origin_test.go b/internal/protocols/httpp/handler_origin_test.go index 4e6379f0..98b0ad91 100644 --- a/internal/protocols/httpp/handler_origin_test.go +++ b/internal/protocols/httpp/handler_origin_test.go @@ -38,7 +38,7 @@ func TestHandlerOrigin(t *testing.T) { "everything allowed, with origin", "https://example.com", []string{"*"}, - "https://example.com", + "*", }, { "allowed", @@ -52,6 +52,12 @@ func TestHandlerOrigin(t *testing.T) { []string{"https://*.example.org"}, "https://test.example.org", }, + { + "everything allowed plus specific domain", + "https://example.org", + []string{"*", "https://example.org"}, + "https://example.org", + }, } { t.Run(ca.name, func(t *testing.T) { s := &Server{ diff --git a/internal/servers/hls/server_test.go b/internal/servers/hls/server_test.go index bb1f84fe..781eee0f 100644 --- a/internal/servers/hls/server_test.go +++ b/internal/servers/hls/server_test.go @@ -98,7 +98,6 @@ func TestServerPreflightRequest(t *testing.T) { require.NoError(t, err) require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) - require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods")) require.Equal(t, "Authorization, Range", res.Header.Get("Access-Control-Allow-Headers")) require.Equal(t, byts, []byte{}) diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index eead0563..f2789fda 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -137,7 +137,6 @@ func TestPreflightRequest(t *testing.T) { require.NoError(t, err) require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) - require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) require.Equal(t, "OPTIONS, GET, POST, PATCH, DELETE", res.Header.Get("Access-Control-Allow-Methods")) require.Equal(t, "Authorization, Content-Type, If-Match", res.Header.Get("Access-Control-Allow-Headers")) require.Equal(t, byts, []byte{})