Skip to content

Commit

Permalink
Bump bundled llhttp to 9.2.1 (#113)
Browse files Browse the repository at this point in the history
CVE-2024-27982

Expose leniency flags via the new `set_dangerous_leniencies` parser
method if somebody needs to opt into the old vulnerable behavior.

Fixes: #111
  • Loading branch information
elprans authored Oct 16, 2024
1 parent 21a199d commit 560bd9e
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 3 deletions.
11 changes: 11 additions & 0 deletions httptools/parser/cparser.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,14 @@ cdef extern from "llhttp.h":
const char* llhttp_method_name(llhttp_method_t method)

void llhttp_set_error_reason(llhttp_t* parser, const char* reason);

void llhttp_set_lenient_headers(llhttp_t* parser, bint enabled);
void llhttp_set_lenient_chunked_length(llhttp_t* parser, bint enabled);
void llhttp_set_lenient_keep_alive(llhttp_t* parser, bint enabled);
void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, bint enabled);
void llhttp_set_lenient_version(llhttp_t* parser, bint enabled);
void llhttp_set_lenient_data_after_close(llhttp_t* parser, bint enabled);
void llhttp_set_lenient_optional_lf_after_cr(llhttp_t* parser, bint enabled);
void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, bint enabled);
void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, bint enabled);
void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, bint enabled);
49 changes: 48 additions & 1 deletion httptools/parser/parser.pyx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#cython: language_level=3

from __future__ import print_function
from typing import Optional

from cpython.mem cimport PyMem_Malloc, PyMem_Free
from cpython cimport PyObject_GetBuffer, PyBuffer_Release, PyBUF_SIMPLE, \
Py_buffer, PyBytes_AsString
Expand Down Expand Up @@ -144,6 +146,51 @@ cdef class HttpParser:

### Public API ###

def set_dangerous_leniencies(
self,
lenient_headers: Optional[bool] = None,
lenient_chunked_length: Optional[bool] = None,
lenient_keep_alive: Optional[bool] = None,
lenient_transfer_encoding: Optional[bool] = None,
lenient_version: Optional[bool] = None,
lenient_data_after_close: Optional[bool] = None,
lenient_optional_lf_after_cr: Optional[bool] = None,
lenient_optional_cr_before_lf: Optional[bool] = None,
lenient_optional_crlf_after_chunk: Optional[bool] = None,
lenient_spaces_after_chunk_size: Optional[bool] = None,
):
cdef cparser.llhttp_t* parser = self._cparser
if lenient_headers is not None:
cparser.llhttp_set_lenient_headers(
parser, lenient_headers)
if lenient_chunked_length is not None:
cparser.llhttp_set_lenient_chunked_length(
parser, lenient_chunked_length)
if lenient_keep_alive is not None:
cparser.llhttp_set_lenient_keep_alive(
parser, lenient_keep_alive)
if lenient_transfer_encoding is not None:
cparser.llhttp_set_lenient_transfer_encoding(
parser, lenient_transfer_encoding)
if lenient_version is not None:
cparser.llhttp_set_lenient_version(
parser, lenient_version)
if lenient_data_after_close is not None:
cparser.llhttp_set_lenient_data_after_close(
parser, lenient_data_after_close)
if lenient_optional_lf_after_cr is not None:
cparser.llhttp_set_lenient_optional_lf_after_cr(
parser, lenient_optional_lf_after_cr)
if lenient_optional_cr_before_lf is not None:
cparser.llhttp_set_lenient_optional_cr_before_lf(
parser, lenient_optional_cr_before_lf)
if lenient_optional_crlf_after_chunk is not None:
cparser.llhttp_set_lenient_optional_crlf_after_chunk(
parser, lenient_optional_crlf_after_chunk)
if lenient_spaces_after_chunk_size is not None:
cparser.llhttp_set_lenient_spaces_after_chunk_size(
parser, lenient_spaces_after_chunk_size)

def get_http_version(self):
cdef cparser.llhttp_t* parser = self._cparser
return '{}.{}'.format(parser.http_major, parser.http_minor)
Expand All @@ -161,7 +208,7 @@ cdef class HttpParser:
cparser.llhttp_errno_t err
Py_buffer *buf
bint owning_buf = False
char* err_pos
const char* err_pos

if PyMemoryView_Check(data):
buf = PyMemoryView_GET_BUFFER(data)
Expand Down
61 changes: 60 additions & 1 deletion tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@

RESPONSE1_HEAD = b'''HTTP/1.1 200 OK
Date: Mon, 23 May 2005 22:38:34 GMT
Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
ETag: "3f80f-1b6-3e1cb03b"
Content-Type: text/html; charset=UTF-8
Content-Length: 130
Accept-Ranges: bytes
Connection: close
'''.replace(b'\n', b'\r\n')

RESPONSE1_SPACES_IN_HEAD = b'''HTTP/1.1 200 OK
Date: Mon, 23 May 2005 22:38:34 GMT
Server: Apache/1.3.3.7
(Unix) (Red-Hat/Linux)
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
Expand Down Expand Up @@ -89,7 +101,7 @@ def test_parser_response_1(self):
self.assertEqual(len(headers), 8)
self.assertEqual(headers.get(b'Connection'), b'close')
self.assertEqual(headers.get(b'Content-Type'),
b'text/html; charset=UTF-8')
b'text/html; charset=UTF-8')

self.assertFalse(m.on_body.called)
p.feed_data(bytearray(RESPONSE1_BODY))
Expand All @@ -109,6 +121,53 @@ def test_parser_response_1b(self):
'Expected HTTP/'):
p.feed_data(b'12123123')

def test_parser_response_leninent_headers_1(self):
m = mock.Mock()

headers = {}
m.on_header.side_effect = headers.__setitem__

p = httptools.HttpResponseParser(m)

with self.assertRaisesRegex(
httptools.HttpParserError,
"whitespace after header value",
):
p.feed_data(memoryview(RESPONSE1_SPACES_IN_HEAD))

def test_parser_response_leninent_headers_2(self):
m = mock.Mock()

headers = {}
m.on_header.side_effect = headers.__setitem__

p = httptools.HttpResponseParser(m)

p.set_dangerous_leniencies(lenient_headers=True)
p.feed_data(memoryview(RESPONSE1_SPACES_IN_HEAD))

self.assertEqual(p.get_http_version(), '1.1')
self.assertEqual(p.get_status_code(), 200)

m.on_status.assert_called_once_with(b'OK')

m.on_headers_complete.assert_called_once_with()
self.assertEqual(m.on_header.call_count, 8)
self.assertEqual(len(headers), 8)
self.assertEqual(headers.get(b'Connection'), b'close')
self.assertEqual(headers.get(b'Content-Type'),
b'text/html; charset=UTF-8')

self.assertFalse(m.on_body.called)
p.feed_data(bytearray(RESPONSE1_BODY))
m.on_body.assert_called_once_with(RESPONSE1_BODY)

m.on_message_complete.assert_called_once_with()

self.assertFalse(m.on_url.called)
self.assertFalse(m.on_chunk_header.called)
self.assertFalse(m.on_chunk_complete.called)

def test_parser_response_2(self):
with self.assertRaisesRegex(TypeError, 'a bytes-like object'):
httptools.HttpResponseParser(None).feed_data('')
Expand Down
2 changes: 1 addition & 1 deletion vendor/llhttp
Submodule llhttp updated 6 files
+8 −6 CMakeLists.txt
+96 −29 README.md
+101 −13 include/llhttp.h
+49 −1 src/api.c
+23 −3 src/http.c
+1,530 −10,044 src/llhttp.c

0 comments on commit 560bd9e

Please sign in to comment.