Summary
process_line() in libavformat/http.c honors whichever of Content-Length and Transfer-Encoding: chunked arrives first in the response headers, instead of letting Transfer-Encoding take precedence as required by RFC 9112 section 6.3. When a server sends Transfer-Encoding: chunked followed by a (bogus or stale) Content-Length, the Content-Length value is accepted into s->filesize and later used as an EOF boundary by http_buf_read(). The chunked body is then silently truncated at that byte count: the read loop returns AVERROR_EOF mid-chunk and the remainder of the body is discarded with no error reported to the caller. If the same two headers arrive in the opposite order, the code behaves correctly — proving the order dependence.
Location
process_line() — Content-Length branch:
|
} else if (!av_strcasecmp(tag, "Content-Length") && |
|
s->filesize == UINT64_MAX) { |
|
s->filesize = strtoull(p, NULL, 10); |
process_line() — Transfer-Encoding branch:
|
} else if (!av_strcasecmp(tag, "Transfer-Encoding") && |
|
!av_strncasecmp(p, "chunked", 7)) { |
|
s->filesize = UINT64_MAX; |
|
s->chunksize = 0; |
http_read_header() — chunksize sentinel initialization:
|
s->chunksize = UINT64_MAX; |
http_buf_read() — where the truncation manifests:
|
uint64_t file_end = s->end_off ? s->end_off : s->filesize; |
|
uint64_t target_end = s->range_end ? s->range_end : file_end; |
|
if ((!s->willclose || s->chunksize == UINT64_MAX) && s->off >= file_end) |
|
return AVERROR_EOF; |
Details
Header parsing in process_line():
} else if (!av_strcasecmp(tag, "Content-Length") &&
s->filesize == UINT64_MAX) {
s->filesize = strtoull(p, NULL, 10);
} else if (!av_strcasecmp(tag, "Content-Range")) {
...
} else if (!av_strcasecmp(tag, "Transfer-Encoding") &&
!av_strncasecmp(p, "chunked", 7)) {
s->filesize = UINT64_MAX;
s->chunksize = 0;
EOF check in http_buf_read() (once the internal header buffer is drained):
} else {
uint64_t file_end = s->end_off ? s->end_off : s->filesize;
uint64_t target_end = s->range_end ? s->range_end : file_end;
if ((!s->willclose || s->chunksize == UINT64_MAX) && s->off >= file_end)
return AVERROR_EOF;
Step-by-step trace (trigger: an HTTP/1.1 keep-alive response carrying both headers, with Transfer-Encoding first):
- Before the request,
s->filesize is initialized to UINT64_MAX (line 816), and http_read_header() sets s->chunksize = UINT64_MAX (line 1483) before parsing the response headers.
- The server responds with headers in this order:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Length: 100
- The
Transfer-Encoding branch (lines 1321-1324) runs first: s->filesize = UINT64_MAX; s->chunksize = 0; — chunked mode is correctly enabled.
- The
Content-Length branch (lines 1312-1314) then runs. Its only guard is s->filesize == UINT64_MAX, which passes because the Transfer-Encoding branch just reset it. s->filesize becomes 100 even though the message is chunked. (In the reverse header order, the Transfer-Encoding branch runs last and wipes the Content-Length, which is the correct outcome — hence the order dependence.)
- During body reads,
http_buf_read() correctly follows chunk framing (s->chunksize != UINT64_MAX), but once the internal buffer is empty it evaluates lines 1824-1827: file_end = s->filesize = 100, and for a keep-alive response s->willclose == 0, so !s->willclose is true regardless of chunked state. As soon as the decoded offset s->off reaches 100, the function returns AVERROR_EOF mid-chunk.
- The caller sees a clean EOF at 100 bytes; the rest of the chunked body is silently dropped. No error or warning is produced.
Note the asymmetric guard: the EOF condition (!s->willclose || s->chunksize == UINT64_MAX) keeps the check enabled for non-closing connections even when chunked, whereas the premature-end check just below it (lines 1831-1838) is correspondingly suppressed, so the truncation is never flagged.
Impact
Severity: medium.
- Silent data truncation: demuxers receive a clean EOF mid-stream and return partial media data with no error. Depending on the container this surfaces as truncated playback, missing samples/packets, or downstream "Invalid data" errors far from the root cause.
- Trigger is server-controlled: any misbehaving or misconfigured origin/proxy/CDN that emits both headers in this order (a real-world occurrence, typically a stale
Content-Length left in place when chunking is applied) triggers the bug. RFC 9112 explicitly anticipates such messages and requires Transfer-Encoding to win.
- Not directly exploitable for memory corruption, but RFC 9112 treats CL/TE conflicts as smuggling-adjacent and mandates ignoring Content-Length precisely to avoid this class of desync.
This code is inherited verbatim from upstream FFmpeg — the identical Content-Length/Transfer-Encoding branches and http_buf_read() EOF logic exist in upstream/master (git show upstream/master:libavformat/http.c, lines 1271-1283). It is worth reporting upstream (trac / ffmpeg-devel) as well.
Suggested fix
In the Content-Length branch, additionally require that no chunked Transfer-Encoding has been seen. s->chunksize is reset to the UINT64_MAX sentinel at the start of every header parse (line 1483) and is only set to 0 by the Transfer-Encoding branch, so it is a reliable "chunked seen" flag at this point:
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -1310,7 +1310,8 @@ static int process_line(URLContext *h, char *line, int line_count, int *parsed_
if ((ret = parse_location(s, p)) < 0)
return ret;
} else if (!av_strcasecmp(tag, "Content-Length") &&
- s->filesize == UINT64_MAX) {
+ s->filesize == UINT64_MAX &&
+ s->chunksize == UINT64_MAX) {
s->filesize = strtoull(p, NULL, 10);
} else if (!av_strcasecmp(tag, "Content-Range")) {
parse_content_range(h, p);
This makes the parser ignore Content-Length whenever chunked encoding is in effect, regardless of header order, matching RFC 9112 section 6.3. (The Content-Range-derived filesize path at lines 1521-1524 is unaffected, as it intentionally applies even with chunked encoding.)
Summary
process_line()inlibavformat/http.chonors whichever ofContent-LengthandTransfer-Encoding: chunkedarrives first in the response headers, instead of lettingTransfer-Encodingtake precedence as required by RFC 9112 section 6.3. When a server sendsTransfer-Encoding: chunkedfollowed by a (bogus or stale)Content-Length, the Content-Length value is accepted intos->filesizeand later used as an EOF boundary byhttp_buf_read(). The chunked body is then silently truncated at that byte count: the read loop returnsAVERROR_EOFmid-chunk and the remainder of the body is discarded with no error reported to the caller. If the same two headers arrive in the opposite order, the code behaves correctly — proving the order dependence.Location
process_line()— Content-Length branch:FFmpeg/libavformat/http.c
Lines 1312 to 1314 in 9a83bff
process_line()— Transfer-Encoding branch:FFmpeg/libavformat/http.c
Lines 1321 to 1324 in 9a83bff
http_read_header()— chunksize sentinel initialization:FFmpeg/libavformat/http.c
Line 1483 in 9a83bff
http_buf_read()— where the truncation manifests:FFmpeg/libavformat/http.c
Lines 1824 to 1827 in 9a83bff
Details
Header parsing in
process_line():EOF check in
http_buf_read()(once the internal header buffer is drained):Step-by-step trace (trigger: an HTTP/1.1 keep-alive response carrying both headers, with
Transfer-Encodingfirst):s->filesizeis initialized toUINT64_MAX(line 816), andhttp_read_header()setss->chunksize = UINT64_MAX(line 1483) before parsing the response headers.Transfer-Encodingbranch (lines 1321-1324) runs first:s->filesize = UINT64_MAX; s->chunksize = 0;— chunked mode is correctly enabled.Content-Lengthbranch (lines 1312-1314) then runs. Its only guard iss->filesize == UINT64_MAX, which passes because the Transfer-Encoding branch just reset it.s->filesizebecomes100even though the message is chunked. (In the reverse header order, the Transfer-Encoding branch runs last and wipes the Content-Length, which is the correct outcome — hence the order dependence.)http_buf_read()correctly follows chunk framing (s->chunksize != UINT64_MAX), but once the internal buffer is empty it evaluates lines 1824-1827:file_end = s->filesize = 100, and for a keep-alive responses->willclose == 0, so!s->willcloseis true regardless of chunked state. As soon as the decoded offsets->offreaches 100, the function returnsAVERROR_EOFmid-chunk.Note the asymmetric guard: the EOF condition
(!s->willclose || s->chunksize == UINT64_MAX)keeps the check enabled for non-closing connections even when chunked, whereas the premature-end check just below it (lines 1831-1838) is correspondingly suppressed, so the truncation is never flagged.Impact
Severity: medium.
Content-Lengthleft in place when chunking is applied) triggers the bug. RFC 9112 explicitly anticipates such messages and requires Transfer-Encoding to win.This code is inherited verbatim from upstream FFmpeg — the identical
Content-Length/Transfer-Encodingbranches andhttp_buf_read()EOF logic exist inupstream/master(git show upstream/master:libavformat/http.c, lines 1271-1283). It is worth reporting upstream (trac / ffmpeg-devel) as well.Suggested fix
In the
Content-Lengthbranch, additionally require that no chunkedTransfer-Encodinghas been seen.s->chunksizeis reset to theUINT64_MAXsentinel at the start of every header parse (line 1483) and is only set to0by the Transfer-Encoding branch, so it is a reliable "chunked seen" flag at this point:This makes the parser ignore
Content-Lengthwhenever chunked encoding is in effect, regardless of header order, matching RFC 9112 section 6.3. (TheContent-Range-derived filesize path at lines 1521-1524 is unaffected, as it intentionally applies even with chunked encoding.)