From f61e8949f10a0e77ceba28a6639627e6e57bf167 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Jun 2026 00:33:03 -0500 Subject: [PATCH] [PR #12826/36df6c13 backport][3.14] Enforce max_line_size on fragmented request target and reason in C parser (#12837) CVE: CVE-2026-54277 Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/5ab61bb4cd88f19b712f12c7c9295fe262bf804d] Backport Changes: - Added aiohttp/_http_parser.c because Wrynose builds the pre-generated C parser without running Cython. - Updated cb_on_url() and cb_on_status() to enforce max_line_size against the accumulated parser buffer. (cherry picked from commit 5ab61bb4cd88f19b712f12c7c9295fe262bf804d) Signed-off-by: Darsh Kelaiya --- CHANGES/12826.bugfix.rst | 1 + aiohttp/_http_parser.c | 36 ++++++++++++++++++------------------ aiohttp/_http_parser.pyx | 4 ++-- tests/test_http_parser.py | 22 ++++++++++++++++++++++ 4 files changed, 43 insertions(+), 20 deletions(-) create mode 100644 CHANGES/12826.bugfix.rst diff --git a/CHANGES/12826.bugfix.rst b/CHANGES/12826.bugfix.rst new file mode 100644 index 000000000..7e095615d --- /dev/null +++ b/CHANGES/12826.bugfix.rst @@ -0,0 +1 @@ +Fixed the C HTTP parser not enforcing ``max_line_size`` on a request target or response reason phrase that is split across multiple reads; each fragment was checked on its own, so an accumulated line could exceed the limit without raising ``LineTooLong``. The accumulated length is now checked, matching the pure-Python parser -- by :user:`bdraco`. diff --git a/aiohttp/_http_parser.c b/aiohttp/_http_parser.c index 911c9aa65..14c5b3350 100644 --- a/aiohttp/_http_parser.c +++ b/aiohttp/_http_parser.c @@ -14760,7 +14760,7 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_url(llhttp_t *__pyx_v_parser, c * const char *at, size_t length) except -1: * cdef HttpParser pyparser = parser.data # <<<<<<<<<<<<<< * try: - * if length > pyparser._max_line_size: + * if len(pyparser._buf) + length > pyparser._max_line_size: */ __pyx_t_1 = ((PyObject *)__pyx_v_parser->data); __Pyx_INCREF(__pyx_t_1); @@ -14771,7 +14771,7 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_url(llhttp_t *__pyx_v_parser, c * const char *at, size_t length) except -1: * cdef HttpParser pyparser = parser.data * try: # <<<<<<<<<<<<<< - * if length > pyparser._max_line_size: + * if len(pyparser._buf) + length > pyparser._max_line_size: * status = pyparser._buf + at[:length] */ { @@ -14786,16 +14786,16 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_url(llhttp_t *__pyx_v_parser, c /* "aiohttp/_http_parser.pyx":721 * cdef HttpParser pyparser = parser.data * try: - * if length > pyparser._max_line_size: # <<<<<<<<<<<<<< + * if len(pyparser._buf) + length > pyparser._max_line_size: # <<<<<<<<<<<<<< * status = pyparser._buf + at[:length] * raise LineTooLong(status[:100] + b"...", pyparser._max_line_size) */ - __pyx_t_5 = (__pyx_v_length > __pyx_v_pyparser->_max_line_size); + __pyx_t_5 = ((__Pyx_PyByteArray_GET_SIZE(__pyx_v_pyparser->_buf) + __pyx_v_length) > __pyx_v_pyparser->_max_line_size); if (unlikely(__pyx_t_5)) { /* "aiohttp/_http_parser.pyx":722 * try: - * if length > pyparser._max_line_size: + * if len(pyparser._buf) + length > pyparser._max_line_size: * status = pyparser._buf + at[:length] # <<<<<<<<<<<<<< * raise LineTooLong(status[:100] + b"...", pyparser._max_line_size) * extend(pyparser._buf, at, length) @@ -14809,7 +14809,7 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_url(llhttp_t *__pyx_v_parser, c __pyx_t_6 = 0; /* "aiohttp/_http_parser.pyx":723 - * if length > pyparser._max_line_size: + * if len(pyparser._buf) + length > pyparser._max_line_size: * status = pyparser._buf + at[:length] * raise LineTooLong(status[:100] + b"...", pyparser._max_line_size) # <<<<<<<<<<<<<< * extend(pyparser._buf, at, length) @@ -14854,7 +14854,7 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_url(llhttp_t *__pyx_v_parser, c /* "aiohttp/_http_parser.pyx":721 * cdef HttpParser pyparser = parser.data * try: - * if length > pyparser._max_line_size: # <<<<<<<<<<<<<< + * if len(pyparser._buf) + length > pyparser._max_line_size: # <<<<<<<<<<<<<< * status = pyparser._buf + at[:length] * raise LineTooLong(status[:100] + b"...", pyparser._max_line_size) */ @@ -14878,7 +14878,7 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_url(llhttp_t *__pyx_v_parser, c * const char *at, size_t length) except -1: * cdef HttpParser pyparser = parser.data * try: # <<<<<<<<<<<<<< - * if length > pyparser._max_line_size: + * if len(pyparser._buf) + length > pyparser._max_line_size: * status = pyparser._buf + at[:length] */ } @@ -14968,7 +14968,7 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_url(llhttp_t *__pyx_v_parser, c * const char *at, size_t length) except -1: * cdef HttpParser pyparser = parser.data * try: # <<<<<<<<<<<<<< - * if length > pyparser._max_line_size: + * if len(pyparser._buf) + length > pyparser._max_line_size: * status = pyparser._buf + at[:length] */ __pyx_L5_except_error:; @@ -15045,7 +15045,7 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_status(llhttp_t *__pyx_v_parser * const char *at, size_t length) except -1: * cdef HttpParser pyparser = parser.data # <<<<<<<<<<<<<< * try: - * if length > pyparser._max_line_size: + * if len(pyparser._buf) + length > pyparser._max_line_size: */ __pyx_t_1 = ((PyObject *)__pyx_v_parser->data); __Pyx_INCREF(__pyx_t_1); @@ -15056,7 +15056,7 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_status(llhttp_t *__pyx_v_parser * const char *at, size_t length) except -1: * cdef HttpParser pyparser = parser.data * try: # <<<<<<<<<<<<<< - * if length > pyparser._max_line_size: + * if len(pyparser._buf) + length > pyparser._max_line_size: * reason = pyparser._buf + at[:length] */ { @@ -15071,16 +15071,16 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_status(llhttp_t *__pyx_v_parser /* "aiohttp/_http_parser.pyx":736 * cdef HttpParser pyparser = parser.data * try: - * if length > pyparser._max_line_size: # <<<<<<<<<<<<<< + * if len(pyparser._buf) + length > pyparser._max_line_size: # <<<<<<<<<<<<<< * reason = pyparser._buf + at[:length] * raise LineTooLong(reason[:100] + b"...", pyparser._max_line_size) */ - __pyx_t_5 = (__pyx_v_length > __pyx_v_pyparser->_max_line_size); + __pyx_t_5 = ((__Pyx_PyByteArray_GET_SIZE(__pyx_v_pyparser->_buf) + __pyx_v_length) > __pyx_v_pyparser->_max_line_size); if (unlikely(__pyx_t_5)) { /* "aiohttp/_http_parser.pyx":737 * try: - * if length > pyparser._max_line_size: + * if len(pyparser._buf) + length > pyparser._max_line_size: * reason = pyparser._buf + at[:length] # <<<<<<<<<<<<<< * raise LineTooLong(reason[:100] + b"...", pyparser._max_line_size) * extend(pyparser._buf, at, length) @@ -15094,7 +15094,7 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_status(llhttp_t *__pyx_v_parser __pyx_t_6 = 0; /* "aiohttp/_http_parser.pyx":738 - * if length > pyparser._max_line_size: + * if len(pyparser._buf) + length > pyparser._max_line_size: * reason = pyparser._buf + at[:length] * raise LineTooLong(reason[:100] + b"...", pyparser._max_line_size) # <<<<<<<<<<<<<< * extend(pyparser._buf, at, length) @@ -15139,7 +15139,7 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_status(llhttp_t *__pyx_v_parser /* "aiohttp/_http_parser.pyx":736 * cdef HttpParser pyparser = parser.data * try: - * if length > pyparser._max_line_size: # <<<<<<<<<<<<<< + * if len(pyparser._buf) + length > pyparser._max_line_size: # <<<<<<<<<<<<<< * reason = pyparser._buf + at[:length] * raise LineTooLong(reason[:100] + b"...", pyparser._max_line_size) */ @@ -15163,7 +15163,7 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_status(llhttp_t *__pyx_v_parser * const char *at, size_t length) except -1: * cdef HttpParser pyparser = parser.data * try: # <<<<<<<<<<<<<< - * if length > pyparser._max_line_size: + * if len(pyparser._buf) + length > pyparser._max_line_size: * reason = pyparser._buf + at[:length] */ } @@ -15253,7 +15253,7 @@ static int __pyx_f_7aiohttp_12_http_parser_cb_on_status(llhttp_t *__pyx_v_parser * const char *at, size_t length) except -1: * cdef HttpParser pyparser = parser.data * try: # <<<<<<<<<<<<<< - * if length > pyparser._max_line_size: + * if len(pyparser._buf) + length > pyparser._max_line_size: * reason = pyparser._buf + at[:length] */ __pyx_L5_except_error:; diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 5da835bc6..e1edee310 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -718,7 +718,7 @@ cdef int cb_on_url(cparser.llhttp_t* parser, const char *at, size_t length) except -1: cdef HttpParser pyparser = parser.data try: - if length > pyparser._max_line_size: + if len(pyparser._buf) + length > pyparser._max_line_size: status = pyparser._buf + at[:length] raise LineTooLong(status[:100] + b"...", pyparser._max_line_size) extend(pyparser._buf, at, length) @@ -733,7 +733,7 @@ cdef int cb_on_status(cparser.llhttp_t* parser, const char *at, size_t length) except -1: cdef HttpParser pyparser = parser.data try: - if length > pyparser._max_line_size: + if len(pyparser._buf) + length > pyparser._max_line_size: reason = pyparser._buf + at[:length] raise LineTooLong(reason[:100] + b"...", pyparser._max_line_size) extend(pyparser._buf, at, length) diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 25604dbcc..8cb591f20 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -1272,6 +1272,17 @@ def test_http_request_max_status_line_under_limit(parser: HttpRequestParser) -> assert msg.url == URL("/path" + path.decode()) +def test_http_request_max_status_line_fragmented( + parser: HttpRequestParser, +) -> None: + # Split an overlong request target across reads so that each callback + # fragment is under the limit but the accumulated target is not. + match = "400, message:\n Got more than 8190 bytes when reading" + with pytest.raises(http_exceptions.LineTooLong, match=match): + parser.feed_data(b"GET /" + b"a" * 8000) + parser.feed_data(b"a" * 8000 + b" HTTP/1.1\r\nHost: a\r\n\r\n") + + def test_http_response_parser_utf8(response) -> None: text = "HTTP/1.1 200 Ok\r\nx-test:ั‚ะตัั‚\r\n\r\n".encode() @@ -1349,6 +1360,17 @@ def test_http_response_parser_status_line_under_limit( assert msg.reason == reason.decode() +def test_http_response_parser_status_line_too_long_fragmented( + response: HttpResponseParser, +) -> None: + # Split an overlong reason phrase across reads so that each callback + # fragment is under the limit but the accumulated reason is not. + match = "400, message:\n Got more than 8190 bytes when reading" + with pytest.raises(http_exceptions.LineTooLong, match=match): + response.feed_data(b"HTTP/1.1 200 " + b"a" * 8000) + response.feed_data(b"a" * 8000 + b"\r\n\r\n") + + def test_http_response_parser_bad_version(response) -> None: with pytest.raises(http_exceptions.BadHttpMessage): response.feed_data(b"HT/11 200 Ok\r\n\r\n") -- 2.35.6