PR #24554 opened by michaelni
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24554
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24554.patch

Fixes: cookie leak to paths sharing a name prefix
Fixes: u2ys08MO8oEF
Found-by: Yazan Balawneh, Cystack.ps


>From 997b529c3c8c530a7424ffccfc3fbc8c160fbcd6 Mon Sep 17 00:00:00 2001
From: Michael Niedermayer <[email protected]>
Date: Thu, 17 Sep 2026 18:53:40 +0200
Subject: [PATCH 1/5] avformat/http: match cookies against the host name, not
 host:port

get_cookies() was handed the "host:port" string used for the Host header
and compared it with the cookie Domain attribute, so a cookie carrying a
Domain never matched a URL with an explicit port. RFC 6265 section 8.5
ignores ports for cookie scoping.

Found during triage/review of security report u2ys08MO8oEF
---
 libavformat/http.c | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/libavformat/http.c b/libavformat/http.c
index fff0f25e36..dcc464f969 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -91,6 +91,7 @@ typedef struct HTTPContext {
     uint8_t *post_data;
     int post_datalen;
     char *cookies;          ///< holds newline (\n) delimited Set-Cookie 
header field values (without the "Set-Cookie: " field name)
+    char *host;
     int icy;
     char *icy_metadata_headers;
     char *icy_metadata_packet;
@@ -262,6 +263,11 @@ static int http_open_cnx_internal(URLContext *h, 
AVDictionary **options)
                  hostname, sizeof(hostname), &port,
                  path1, sizeof(path1), s->location);
 
+    av_freep(&s->host);
+    s->host = av_strdup(hostname);
+    if (!s->host)
+        return AVERROR(ENOMEM);
+
     av_strlcpy(tmp_host, hostname, sizeof(tmp_host));
     // In case of an IPv6 address, we need to strip the Zone ID,
     // if any. We do it at the first % sign, as percent encoding
@@ -1393,8 +1399,7 @@ static int process_line(URLContext *h, char *line, int 
line_count, int *parsed_h
  *
  * @return a negative value if an error condition occurred, 0 otherwise
  */
-static int get_cookies(HTTPContext *s, char **cookies, const char *path,
-                       const char *domain)
+static int get_cookies(HTTPContext *s, char **cookies, const char *path)
 {
     // cookie strings will look like Set-Cookie header field values.  Multiple
     // Set-Cookie fields will result in multiple values delimited by a newline
@@ -1443,12 +1448,12 @@ static int get_cookies(HTTPContext *s, char **cookies, 
const char *path,
         // if no domain in the cookie assume it applied to this request
         if ((e = av_dict_get(cookie_params, "domain", NULL, 0)) && e->value) {
             // find the offset comparison is on the min domain (b.com, not 
a.b.com)
-            int domain_offset = strlen(domain) - strlen(e->value);
+            int domain_offset = strlen(s->host) - strlen(e->value);
             if (domain_offset < 0)
                 goto skip_cookie;
 
             // match the cookie domain
-            if (av_strcasecmp(&domain[domain_offset], e->value))
+            if (av_strcasecmp(&s->host[domain_offset], e->value))
                 goto skip_cookie;
         }
 
@@ -1689,7 +1694,7 @@ static int http_connect(URLContext *h, const char *path, 
const char *local_path,
         av_bprintf(&request, "Content-Type: %s\r\n", s->content_type);
     if (!has_header(s->headers, "\r\nCookie: ") && s->cookies) {
         char *cookies = NULL;
-        if (!get_cookies(s, &cookies, path, hoststr) && cookies) {
+        if (!get_cookies(s, &cookies, path) && cookies) {
             av_bprintf(&request, "Cookie: %s\r\n", cookies);
             av_free(cookies);
         }
@@ -2141,6 +2146,7 @@ static int http_close(URLContext *h)
     av_dict_free(&s->redirect_cache);
     av_freep(&s->new_location);
     av_freep(&s->uri);
+    av_freep(&s->host);
 
     av_log(h, AV_LOG_DEBUG, "Statistics: %d connection%s, %d request%s, %d 
retr%s, %d reconnection%s, %d redirect%s\n",
            s->nb_connections, s->nb_connections == 1 ? ""  : "s",
-- 
2.52.0


>From d38c9a04b7b2ebdddc6bbece515ae5bdadc42669 Mon Sep 17 00:00:00 2001
From: Michael Niedermayer <[email protected]>
Date: Thu, 17 Sep 2026 18:53:41 +0200
Subject: [PATCH 2/5] avformat/http: match cookie domains only at a label
 boundary

A cookie with Domain=example.com was sent to any host whose name merely
ends in "example.com", such as evilexample.com, because get_cookies()
only compared the suffix. RFC 6265 section 5.1.3 requires the domain to
equal the host or to be a suffix of it preceded by a dot.

Fixes: cookie leak to hosts sharing a name suffix
Fixes: u2ys08MO8oEF
Found-by: Yazan Balawneh, Cystack.ps
---
 libavformat/http.c | 30 ++++++++++++++++++++----------
 1 file changed, 20 insertions(+), 10 deletions(-)

diff --git a/libavformat/http.c b/libavformat/http.c
index dcc464f969..b7bd928526 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -1102,6 +1102,22 @@ static int parse_set_cookie(const char *set_cookie, 
AVDictionary **dict)
     return 0;
 }
 
+static const char *cookie_domain(const AVDictionary *cookie_params)
+{
+    const AVDictionaryEntry *e = av_dict_get(cookie_params, "domain", NULL, 0);
+    const char *domain = e ? e->value + (e->value[0] == '.') : "";
+
+    return *domain ? domain : NULL;
+}
+
+static int host_in_cookie_domain(const char *host, const char *domain)
+{
+    int offset = strlen(host) - strlen(domain);
+
+    return offset >= 0 && !av_strcasecmp(host + offset, domain) &&
+           (!offset || host[offset - 1] == '.');
+}
+
 static int parse_cookie(HTTPContext *s, const char *p, AVDictionary **cookies)
 {
     AVDictionary *new_params = NULL;
@@ -1421,6 +1437,7 @@ static int get_cookies(HTTPContext *s, char **cookies, 
const char *path)
     while ((cookie = av_strtok(next, "\n", &saveptr)) && !ret) {
         AVDictionary *cookie_params = NULL;
         const AVDictionaryEntry *cookie_entry, *e;
+        const char *domain;
 
         next = NULL;
         // store the cookie in a dict in case it is updated in the response
@@ -1446,16 +1463,9 @@ static int get_cookies(HTTPContext *s, char **cookies, 
const char *path)
         }
 
         // if no domain in the cookie assume it applied to this request
-        if ((e = av_dict_get(cookie_params, "domain", NULL, 0)) && e->value) {
-            // find the offset comparison is on the min domain (b.com, not 
a.b.com)
-            int domain_offset = strlen(s->host) - strlen(e->value);
-            if (domain_offset < 0)
-                goto skip_cookie;
-
-            // match the cookie domain
-            if (av_strcasecmp(&s->host[domain_offset], e->value))
-                goto skip_cookie;
-        }
+        domain = cookie_domain(cookie_params);
+        if (domain && !host_in_cookie_domain(s->host, domain))
+            goto skip_cookie;
 
         // if a cookie path is provided, ensure the request path is within 
that path
         e = av_dict_get(cookie_params, "path", NULL, 0);
-- 
2.52.0


>From daefb3e4f05d30a29c82ee6b1d6de88666125851 Mon Sep 17 00:00:00 2001
From: Yazan Balawneh <[email protected]>
Date: Thu, 17 Sep 2026 18:53:42 +0200
Subject: [PATCH 3/5] avformat/http: match cookie paths only at a segment
 boundary

Fixes: cookie leak to paths sharing a name prefix
Fixes: u2ys08MO8oEF
Found-by: Yazan Balawneh, Cystack.ps
---
 libavformat/http.c | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/libavformat/http.c b/libavformat/http.c
index b7bd928526..3d2bcfe3fa 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -1469,8 +1469,13 @@ static int get_cookies(HTTPContext *s, char **cookies, 
const char *path)
 
         // if a cookie path is provided, ensure the request path is within 
that path
         e = av_dict_get(cookie_params, "path", NULL, 0);
-        if (e && av_strncasecmp(path, e->value, strlen(e->value)))
-            goto skip_cookie;
+        if (e) {
+            size_t len = strlen(e->value);
+            if (av_strncasecmp(path, e->value, len) ||
+                (len && path[len] && path[len] != '/' && path[len] != '?' &&
+                 e->value[len - 1] != '/'))
+                goto skip_cookie;
+        }
 
         // cookie parameters match, so copy the value
         if (!*cookies) {
-- 
2.52.0


>From fa73ba72c8fd79b81023d99ad2f5bf76e4db2fb6 Mon Sep 17 00:00:00 2001
From: Michael Niedermayer <[email protected]>
Date: Thu, 17 Sep 2026 18:53:43 +0200
Subject: [PATCH 4/5] avformat/http: send cookies set without a Domain only
 back to their host

A Set-Cookie response without a Domain attribute was stored without any
host restriction, so a redirect, a keep-alive reuse or a multi-host
HLS/DASH manifest replayed the cookie to every later host, leaking
session cookies across origins. RFC 6265 section 5.3 makes such a cookie
host-only: it is returned to the exact host that set it and nothing else.

Fixes: cross-host cookie leak
Fixes: u2ys08MO8oEF
Found-by: Yazan Balawneh, Cystack.ps
---
 libavformat/http.c | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/libavformat/http.c b/libavformat/http.c
index 3d2bcfe3fa..dfa36ce116 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -1118,12 +1118,13 @@ static int host_in_cookie_domain(const char *host, 
const char *domain)
            (!offset || host[offset - 1] == '.');
 }
 
-static int parse_cookie(HTTPContext *s, const char *p, AVDictionary **cookies)
+static int parse_cookie(HTTPContext *s, const char *p, const char *host,
+                        AVDictionary **cookies)
 {
     AVDictionary *new_params = NULL;
     const AVDictionaryEntry *e, *cookie_entry;
     const char *eql;
-    char *name;
+    char *name, *value;
 
     // ensure the cookie is parsable
     if (parse_set_cookie(p, &new_params)) {
@@ -1171,6 +1172,7 @@ static int parse_cookie(HTTPContext *s, const char *p, 
AVDictionary **cookies)
             }
         }
     }
+    int host_only = host && !cookie_domain(new_params);
     av_dict_free(&new_params);
 
     // duplicate the cookie name (dict will dupe the value)
@@ -1178,7 +1180,12 @@ static int parse_cookie(HTTPContext *s, const char *p, 
AVDictionary **cookies)
     if (!(name = av_strndup(p, eql - p))) return AVERROR(ENOMEM);
 
     // add the cookie to the dictionary
-    av_dict_set(cookies, name, eql, AV_DICT_DONT_STRDUP_KEY);
+    value = av_asprintf("%s%s%s", eql, host_only ? "; hostonly=" : "", 
host_only ? host : "");
+    if (!value) {
+        av_free(name);
+        return AVERROR(ENOMEM);
+    }
+    av_dict_set(cookies, name, value, AV_DICT_DONT_STRDUP_KEY | 
AV_DICT_DONT_STRDUP_VAL);
 
     return 0;
 }
@@ -1376,7 +1383,7 @@ static int process_line(URLContext *h, char *line, int 
line_count, int *parsed_h
             av_free(s->mime_type);
             s->mime_type = av_get_token((const char **)&p, ";");
         } else if (!av_strcasecmp(tag, "Set-Cookie")) {
-            if (parse_cookie(s, p, &s->cookie_dict))
+            if (parse_cookie(s, p, s->host, &s->cookie_dict))
                 av_log(h, AV_LOG_WARNING, "Unable to parse '%s'\n", p);
         } else if (!av_strcasecmp(tag, "Icy-MetaInt")) {
             s->icy_metaint = strtoull(p, NULL, 10);
@@ -1441,7 +1448,7 @@ static int get_cookies(HTTPContext *s, char **cookies, 
const char *path)
 
         next = NULL;
         // store the cookie in a dict in case it is updated in the response
-        if (parse_cookie(s, cookie, &s->cookie_dict))
+        if (parse_cookie(s, cookie, NULL, &s->cookie_dict))
             av_log(s, AV_LOG_WARNING, "Unable to parse '%s'\n", cookie);
 
         // continue on to the next cookie if this one cannot be parsed
@@ -1467,6 +1474,9 @@ static int get_cookies(HTTPContext *s, char **cookies, 
const char *path)
         if (domain && !host_in_cookie_domain(s->host, domain))
             goto skip_cookie;
 
+        if ((e = av_dict_get(cookie_params, "hostonly", NULL, 0)) && 
av_strcasecmp(e->value, s->host))
+            goto skip_cookie;
+
         // if a cookie path is provided, ensure the request path is within 
that path
         e = av_dict_get(cookie_params, "path", NULL, 0);
         if (e) {
-- 
2.52.0


>From 7c9feab6223c1ff839121c608a9e3730b06fe022 Mon Sep 17 00:00:00 2001
From: Michael Niedermayer <[email protected]>
Date: Thu, 17 Sep 2026 18:53:44 +0200
Subject: [PATCH 5/5] avformat/http: ignore response cookies whose Domain does
 not cover the host

Fixes: cookie injection for foreign domains
Fixes: u2ys08MO8oEF
Found during triage/review of the security report
---
 libavformat/http.c | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/libavformat/http.c b/libavformat/http.c
index dfa36ce116..b9f609a025 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -1172,7 +1172,13 @@ static int parse_cookie(HTTPContext *s, const char *p, 
const char *host,
             }
         }
     }
-    int host_only = host && !cookie_domain(new_params);
+    const char *domain = host ? cookie_domain(new_params) : NULL;
+    int host_only = host && !domain;
+    if (domain && !host_in_cookie_domain(host, domain)) {
+        av_log(s, AV_LOG_WARNING, "Ignoring cookie for domain %s set by %s\n", 
domain, host);
+        av_dict_free(&new_params);
+        return 0;
+    }
     av_dict_free(&new_params);
 
     // duplicate the cookie name (dict will dupe the value)
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to