[PR] avformat/http: reject malformed response status lines (PR #24232)

Romain Beauxis via ffmpeg-devel <[email protected]>
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <[email protected]>
PR #24232 opened by Romain Beauxis (toots)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24232
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24232.patch

This PR fixes issues with native HTTP parser accepting malformed response status lines.

These issues were uncovered while working on adding `ICY` support to the new `libcurl` implementation.

Before the changes, malformed HTTP status responses were mistakenly taken for valid `200` responses, prompting the parser to accept them and proceed further.

One side-effect was that `ICY 200` requests were accepted (as well as `ICY 404` requests). The changes in this patchset remove this unexpected feature.

Worth noting: `ICY` status response is very old and specific to shoutcast so I don't think that we should worry too much about support them.

Opt-in support for them is planned as part of the upcoming changes to the new `libcurl` backend.

See https://code.ffmpeg.org/toots/FFmpeg/commit/2cfcd0c11dc08b9d8a58c6962e71af69af68ff42 for tests details.


>From 9b76690f4926fcd81a012b49bd161308f244f8a1 Mon Sep 17 00:00:00 2001
From: Romain Beauxis <[email protected]>
Date: Fri, 21 Aug 2026 08:27:16 -0500
Subject: [PATCH 1/3] avformat/http: split the response status line parser out
 of process_line

Makes the parser reachable from a test program.  No functional change, beyond
checking the allocation of the exported version string.
---
 libavformat/http.c | 59 +++++++++++++++++++++++++++++++++-------------
 libavformat/http.h | 21 +++++++++++++++++
 2 files changed, 64 insertions(+), 16 deletions(-)

diff --git a/libavformat/http.c b/libavformat/http.c
index 69d4c8ab72..50fd88970d 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -890,6 +890,34 @@ static int http_get_line(HTTPContext *s, char *line, int line_size)
     }
 }
 
+int ff_http_parse_status_line(void *logctx, const char *line, HTTPStatusLine *st)
+{
+    const char *p = line;
+    char *end;
+
+    memset(st, 0, sizeof(*st));
+
+    if (av_strncasecmp(p, "HTTP/1.0", 8) == 0)
+        st->willclose = 1;
+
+    while (*p != '/' && *p != '\0')
+        p++;
+    while (*p == '/')
+        p++;
+    av_strlcpy(st->version, p, sizeof(st->version));
+
+    while (!av_isspace(*p) && *p != '\0')
+        p++;
+    while (av_isspace(*p))
+        p++;
+    st->code   = strtol(p, &end, 10);
+    st->reason = end;
+
+    av_log(logctx, AV_LOG_TRACE, "http_code=%d\n", st->code);
+
+    return 0;
+}
+
 static int check_http_code(URLContext *h, int http_code, const char *end)
 {
     HTTPContext *s = h->priv_data;
@@ -1181,7 +1209,7 @@ static int process_line(URLContext *h, char *line, int line_count, int *parsed_h
 {
     HTTPContext *s = h->priv_data;
     const char *auto_method =  h->flags & AVIO_FLAG_READ ? "POST" : "GET";
-    char *tag, *p, *end, *method, *resource, *version;
+    char *tag, *p, *method, *resource, *version;
     int ret;
 
     /* end of header */
@@ -1245,25 +1273,24 @@ static int process_line(URLContext *h, char *line, int line_count, int *parsed_h
             }
             av_log(h, AV_LOG_TRACE, "HTTP version string: %s\n", version);
         } else {
-            if (av_strncasecmp(p, "HTTP/1.0", 8) == 0)
-                s->willclose = 1;
-            while (*p != '/' && *p != '\0')
-                p++;
-            while (*p == '/')
-                p++;
-            av_freep(&s->http_version);
-            s->http_version = av_strndup(p, 3);
-            while (!av_isspace(*p) && *p != '\0')
-                p++;
-            while (av_isspace(*p))
-                p++;
-            s->http_code = strtol(p, &end, 10);
+            HTTPStatusLine st;
 
-            av_log(h, AV_LOG_TRACE, "http_code=%d\n", s->http_code);
+            if ((ret = ff_http_parse_status_line(h, p, &st)) < 0)
+                return ret;
+
+            /* Only ever set: a keep-alive decision made earlier must survive. */
+            if (st.willclose)
+                s->willclose = 1;
+
+            av_freep(&s->http_version);
+            if (!(s->http_version = av_strdup(st.version)))
+                return AVERROR(ENOMEM);
+
+            s->http_code = st.code;
 
             *parsed_http_code = 1;
 
-            if ((ret = check_http_code(h, s->http_code, end)) < 0)
+            if ((ret = check_http_code(h, s->http_code, st.reason)) < 0)
                 return ret;
         }
     } else {
diff --git a/libavformat/http.h b/libavformat/http.h
index 08605c8f91..dd2c15789e 100644
--- a/libavformat/http.h
+++ b/libavformat/http.h
@@ -27,6 +27,27 @@
 
 #define HTTP_HEADERS_SIZE 4096
 
+/**
+ * Parsed form of a response status line.
+ */
+typedef struct HTTPStatusLine {
+    char        version[4]; /**< protocol version, e.g. "1.1" */
+    int         code;       /**< response status code */
+    const char *reason;     /**< reason phrase, points into the parsed line */
+    int         willclose;  /**< the version implies the connection will close */
+} HTTPStatusLine;
+
+/**
+ * Parse the status line of an HTTP response.
+ *
+ * @param logctx context used for logging, may be NULL
+ * @param line NUL terminated status line, without its trailing CRLF
+ * @param st filled in with the parsed status line
+ * @return 0 on success, a negative AVERROR code on failure
+ */
+int ff_http_parse_status_line(void *logctx, const char *line,
+                              HTTPStatusLine *st);
+
 /**
  * Initialize the authentication state based on another HTTP URLContext.
  * This can be used to pre-initialize the authentication parameters if
-- 
2.52.0


>From 2cfcd0c11dc08b9d8a58c6962e71af69af68ff42 Mon Sep 17 00:00:00 2001
From: Romain Beauxis <[email protected]>
Date: Fri, 21 Aug 2026 08:27:25 -0500
Subject: [PATCH 2/3] tests/http: test response status line parsing

Covers well formed status lines, a missing or non numeric status code, and
lines that do not start with an HTTP version.
---
 libavformat/Makefile            |  1 +
 libavformat/tests/.gitignore    |  1 +
 libavformat/tests/http.c        | 67 +++++++++++++++++++++++++++++++++
 tests/fate/libavformat.mak      |  4 ++
 tests/ref/fate/http-status-line | 28 ++++++++++++++
 5 files changed, 101 insertions(+)
 create mode 100644 libavformat/tests/http.c
 create mode 100644 tests/ref/fate/http-status-line

diff --git a/libavformat/Makefile b/libavformat/Makefile
index 466f5d1894..038e0afd41 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -790,6 +790,7 @@ TESTPROGS = id3v2                                                       \
 FIFO-MUXER-TESTPROGS-$(CONFIG_NETWORK)   += fifo_muxer
 TESTPROGS-$(CONFIG_FIFO_MUXER)           += $(FIFO-MUXER-TESTPROGS-yes)
 TESTPROGS-$(CONFIG_FFRTMPCRYPT_PROTOCOL) += rtmpdh
+TESTPROGS-$(CONFIG_HTTP_PROTOCOL)        += http
 TESTPROGS-$(CONFIG_NETWORK)              += noproxy
 TESTPROGS-$(CONFIG_SRTP)                 += srtp
 TESTPROGS-$(CONFIG_IMF_DEMUXER)          += imf
diff --git a/libavformat/tests/.gitignore b/libavformat/tests/.gitignore
index 1807488603..2be3cc8fac 100644
--- a/libavformat/tests/.gitignore
+++ b/libavformat/tests/.gitignore
@@ -9,3 +9,4 @@
 /srtp
 /url
 /seek_utils
+/http
diff --git a/libavformat/tests/http.c b/libavformat/tests/http.c
new file mode 100644
index 0000000000..3767cf319b
--- /dev/null
+++ b/libavformat/tests/http.c
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2026 Romain Beauxis
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <stdio.h>
+
+#include "libavformat/http.h"
+
+static void test(const char *line)
+{
+    HTTPStatusLine st;
+    int ret;
+
+    printf("\"%s\"\n", line);
+
+    ret = ff_http_parse_status_line(NULL, line, &st);
+    if (ret < 0) {
+        printf("  rejected\n");
+        return;
+    }
+
+    printf("  version=\"%s\" code=%d willclose=%d reason=\"%s\"\n",
+           st.version, st.code, st.willclose, st.reason);
+}
+
+int main(void)
+{
+    /* Well formed. */
+    test("HTTP/1.1 200 OK");
+    test("HTTP/1.0 200 OK");
+    test("HTTP/1.1 404 Not Found");
+    test("HTTP/1.1 500 Internal Server Error");
+    test("HTTP/1.1 204 No Content");
+
+    /* Well formed, without a reason phrase. */
+    test("HTTP/1.1 200");
+
+    /* Malformed: no version, no status code, or not HTTP at all. */
+    test("HTTP/1.1");
+    test("HTTP/1.1 OK");
+    test("HTTP");
+    test("200 OK");
+    test("NOT A STATUS LINE");
+    test("");
+
+    /* Shoutcast answers with its own status line, which is not HTTP. */
+    test("ICY 200 OK");
+    test("ICY 404 Not Found");
+
+    return 0;
+}
diff --git a/tests/fate/libavformat.mak b/tests/fate/libavformat.mak
index 09649fed11..4125dde0b4 100644
--- a/tests/fate/libavformat.mak
+++ b/tests/fate/libavformat.mak
@@ -12,6 +12,10 @@ fate-rename: libavformat/tests/rename$(EXESUF)
 fate-rename: CMD = run libavformat/tests/rename$(EXESUF)
 fate-rename: CMP = null
 
+FATE_LIBAVFORMAT-$(CONFIG_HTTP_PROTOCOL) += fate-http-status-line
+fate-http-status-line: libavformat/tests/http$(EXESUF)
+fate-http-status-line: CMD = run libavformat/tests/http$(EXESUF)
+
 FATE_LIBAVFORMAT-$(CONFIG_NETWORK) += fate-noproxy
 fate-noproxy: libavformat/tests/noproxy$(EXESUF)
 fate-noproxy: CMD = run libavformat/tests/noproxy$(EXESUF)
diff --git a/tests/ref/fate/http-status-line b/tests/ref/fate/http-status-line
new file mode 100644
index 0000000000..20ce4b0889
--- /dev/null
+++ b/tests/ref/fate/http-status-line
@@ -0,0 +1,28 @@
+"HTTP/1.1 200 OK"
+  version="1.1" code=200 willclose=0 reason=" OK"
+"HTTP/1.0 200 OK"
+  version="1.0" code=200 willclose=1 reason=" OK"
+"HTTP/1.1 404 Not Found"
+  version="1.1" code=404 willclose=0 reason=" Not Found"
+"HTTP/1.1 500 Internal Server Error"
+  version="1.1" code=500 willclose=0 reason=" Internal Server Error"
+"HTTP/1.1 204 No Content"
+  version="1.1" code=204 willclose=0 reason=" No Content"
+"HTTP/1.1 200"
+  version="1.1" code=200 willclose=0 reason=""
+"HTTP/1.1"
+  version="1.1" code=0 willclose=0 reason=""
+"HTTP/1.1 OK"
+  version="1.1" code=0 willclose=0 reason="OK"
+"HTTP"
+  version="" code=0 willclose=0 reason=""
+"200 OK"
+  version="" code=0 willclose=0 reason=""
+"NOT A STATUS LINE"
+  version="" code=0 willclose=0 reason=""
+""
+  version="" code=0 willclose=0 reason=""
+"ICY 200 OK"
+  version="" code=0 willclose=0 reason=""
+"ICY 404 Not Found"
+  version="" code=0 willclose=0 reason=""
-- 
2.52.0


>From 023b69c97bc0ac2b709cfc804abed6d95b375b28 Mon Sep 17 00:00:00 2001
From: Romain Beauxis <[email protected]>
Date: Fri, 21 Aug 2026 08:27:25 -0500
Subject: [PATCH 3/3] avformat/http: reject malformed response status lines

A status line carrying no HTTP version parsed as status code 0, which
check_http_code() then accepted as a success: the body of an error reply, or of
a reply from something that is not an HTTP server at all, was handed to the
demuxer.

The request line parser in the same function already rejects a version string
that does not start with "HTTP/"; do the same for responses, and reject a
status line whose code is missing or not a number.
---
 libavformat/http.c              | 18 ++++++++++++------
 tests/ref/fate/http-status-line | 16 ++++++++--------
 2 files changed, 20 insertions(+), 14 deletions(-)

diff --git a/libavformat/http.c b/libavformat/http.c
index 50fd88970d..26130a20bc 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -897,20 +897,26 @@ int ff_http_parse_status_line(void *logctx, const char *line, HTTPStatusLine *st
 
     memset(st, 0, sizeof(*st));
 
-    if (av_strncasecmp(p, "HTTP/1.0", 8) == 0)
+    if (av_strncasecmp(p, "HTTP/", 5)) {
+        av_log(logctx, AV_LOG_ERROR, "Malformed HTTP status line.\n");
+        return AVERROR_INVALIDDATA;
+    }
+    p += 5;
+
+    if (av_strncasecmp(line, "HTTP/1.0", 8) == 0)
         st->willclose = 1;
 
-    while (*p != '/' && *p != '\0')
-        p++;
-    while (*p == '/')
-        p++;
     av_strlcpy(st->version, p, sizeof(st->version));
 
     while (!av_isspace(*p) && *p != '\0')
         p++;
     while (av_isspace(*p))
         p++;
-    st->code   = strtol(p, &end, 10);
+    st->code = strtol(p, &end, 10);
+    if (end == p) {
+        av_log(logctx, AV_LOG_ERROR, "Malformed HTTP status code.\n");
+        return AVERROR_INVALIDDATA;
+    }
     st->reason = end;
 
     av_log(logctx, AV_LOG_TRACE, "http_code=%d\n", st->code);
diff --git a/tests/ref/fate/http-status-line b/tests/ref/fate/http-status-line
index 20ce4b0889..05ce524573 100644
--- a/tests/ref/fate/http-status-line
+++ b/tests/ref/fate/http-status-line
@@ -11,18 +11,18 @@
 "HTTP/1.1 200"
   version="1.1" code=200 willclose=0 reason=""
 "HTTP/1.1"
-  version="1.1" code=0 willclose=0 reason=""
+  rejected
 "HTTP/1.1 OK"
-  version="1.1" code=0 willclose=0 reason="OK"
+  rejected
 "HTTP"
-  version="" code=0 willclose=0 reason=""
+  rejected
 "200 OK"
-  version="" code=0 willclose=0 reason=""
+  rejected
 "NOT A STATUS LINE"
-  version="" code=0 willclose=0 reason=""
+  rejected
 ""
-  version="" code=0 willclose=0 reason=""
+  rejected
 "ICY 200 OK"
-  version="" code=0 willclose=0 reason=""
+  rejected
 "ICY 404 Not Found"
-  version="" code=0 willclose=0 reason=""
+  rejected
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.