Skip to content

Commit 560bd9e

Browse files
authored
Bump bundled llhttp to 9.2.1 (#113)
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
1 parent 21a199d commit 560bd9e

File tree

4 files changed

+120
-3
lines changed

4 files changed

+120
-3
lines changed

httptools/parser/cparser.pxd

+11
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,14 @@ cdef extern from "llhttp.h":
154154
const char* llhttp_method_name(llhttp_method_t method)
155155

156156
void llhttp_set_error_reason(llhttp_t* parser, const char* reason);
157+
158+
void llhttp_set_lenient_headers(llhttp_t* parser, bint enabled);
159+
void llhttp_set_lenient_chunked_length(llhttp_t* parser, bint enabled);
160+
void llhttp_set_lenient_keep_alive(llhttp_t* parser, bint enabled);
161+
void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, bint enabled);
162+
void llhttp_set_lenient_version(llhttp_t* parser, bint enabled);
163+
void llhttp_set_lenient_data_after_close(llhttp_t* parser, bint enabled);
164+
void llhttp_set_lenient_optional_lf_after_cr(llhttp_t* parser, bint enabled);
165+
void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, bint enabled);
166+
void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, bint enabled);
167+
void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, bint enabled);

httptools/parser/parser.pyx

+48-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#cython: language_level=3
22

33
from __future__ import print_function
4+
from typing import Optional
5+
46
from cpython.mem cimport PyMem_Malloc, PyMem_Free
57
from cpython cimport PyObject_GetBuffer, PyBuffer_Release, PyBUF_SIMPLE, \
68
Py_buffer, PyBytes_AsString
@@ -144,6 +146,51 @@ cdef class HttpParser:
144146

145147
### Public API ###
146148

149+
def set_dangerous_leniencies(
150+
self,
151+
lenient_headers: Optional[bool] = None,
152+
lenient_chunked_length: Optional[bool] = None,
153+
lenient_keep_alive: Optional[bool] = None,
154+
lenient_transfer_encoding: Optional[bool] = None,
155+
lenient_version: Optional[bool] = None,
156+
lenient_data_after_close: Optional[bool] = None,
157+
lenient_optional_lf_after_cr: Optional[bool] = None,
158+
lenient_optional_cr_before_lf: Optional[bool] = None,
159+
lenient_optional_crlf_after_chunk: Optional[bool] = None,
160+
lenient_spaces_after_chunk_size: Optional[bool] = None,
161+
):
162+
cdef cparser.llhttp_t* parser = self._cparser
163+
if lenient_headers is not None:
164+
cparser.llhttp_set_lenient_headers(
165+
parser, lenient_headers)
166+
if lenient_chunked_length is not None:
167+
cparser.llhttp_set_lenient_chunked_length(
168+
parser, lenient_chunked_length)
169+
if lenient_keep_alive is not None:
170+
cparser.llhttp_set_lenient_keep_alive(
171+
parser, lenient_keep_alive)
172+
if lenient_transfer_encoding is not None:
173+
cparser.llhttp_set_lenient_transfer_encoding(
174+
parser, lenient_transfer_encoding)
175+
if lenient_version is not None:
176+
cparser.llhttp_set_lenient_version(
177+
parser, lenient_version)
178+
if lenient_data_after_close is not None:
179+
cparser.llhttp_set_lenient_data_after_close(
180+
parser, lenient_data_after_close)
181+
if lenient_optional_lf_after_cr is not None:
182+
cparser.llhttp_set_lenient_optional_lf_after_cr(
183+
parser, lenient_optional_lf_after_cr)
184+
if lenient_optional_cr_before_lf is not None:
185+
cparser.llhttp_set_lenient_optional_cr_before_lf(
186+
parser, lenient_optional_cr_before_lf)
187+
if lenient_optional_crlf_after_chunk is not None:
188+
cparser.llhttp_set_lenient_optional_crlf_after_chunk(
189+
parser, lenient_optional_crlf_after_chunk)
190+
if lenient_spaces_after_chunk_size is not None:
191+
cparser.llhttp_set_lenient_spaces_after_chunk_size(
192+
parser, lenient_spaces_after_chunk_size)
193+
147194
def get_http_version(self):
148195
cdef cparser.llhttp_t* parser = self._cparser
149196
return '{}.{}'.format(parser.http_major, parser.http_minor)
@@ -161,7 +208,7 @@ cdef class HttpParser:
161208
cparser.llhttp_errno_t err
162209
Py_buffer *buf
163210
bint owning_buf = False
164-
char* err_pos
211+
const char* err_pos
165212

166213
if PyMemoryView_Check(data):
167214
buf = PyMemoryView_GET_BUFFER(data)

tests/test_parser.py

+60-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66

77
RESPONSE1_HEAD = b'''HTTP/1.1 200 OK
88
Date: Mon, 23 May 2005 22:38:34 GMT
9+
Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)
10+
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
11+
ETag: "3f80f-1b6-3e1cb03b"
12+
Content-Type: text/html; charset=UTF-8
13+
Content-Length: 130
14+
Accept-Ranges: bytes
15+
Connection: close
16+
17+
'''.replace(b'\n', b'\r\n')
18+
19+
RESPONSE1_SPACES_IN_HEAD = b'''HTTP/1.1 200 OK
20+
Date: Mon, 23 May 2005 22:38:34 GMT
921
Server: Apache/1.3.3.7
1022
(Unix) (Red-Hat/Linux)
1123
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
@@ -89,7 +101,7 @@ def test_parser_response_1(self):
89101
self.assertEqual(len(headers), 8)
90102
self.assertEqual(headers.get(b'Connection'), b'close')
91103
self.assertEqual(headers.get(b'Content-Type'),
92-
b'text/html; charset=UTF-8')
104+
b'text/html; charset=UTF-8')
93105

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

124+
def test_parser_response_leninent_headers_1(self):
125+
m = mock.Mock()
126+
127+
headers = {}
128+
m.on_header.side_effect = headers.__setitem__
129+
130+
p = httptools.HttpResponseParser(m)
131+
132+
with self.assertRaisesRegex(
133+
httptools.HttpParserError,
134+
"whitespace after header value",
135+
):
136+
p.feed_data(memoryview(RESPONSE1_SPACES_IN_HEAD))
137+
138+
def test_parser_response_leninent_headers_2(self):
139+
m = mock.Mock()
140+
141+
headers = {}
142+
m.on_header.side_effect = headers.__setitem__
143+
144+
p = httptools.HttpResponseParser(m)
145+
146+
p.set_dangerous_leniencies(lenient_headers=True)
147+
p.feed_data(memoryview(RESPONSE1_SPACES_IN_HEAD))
148+
149+
self.assertEqual(p.get_http_version(), '1.1')
150+
self.assertEqual(p.get_status_code(), 200)
151+
152+
m.on_status.assert_called_once_with(b'OK')
153+
154+
m.on_headers_complete.assert_called_once_with()
155+
self.assertEqual(m.on_header.call_count, 8)
156+
self.assertEqual(len(headers), 8)
157+
self.assertEqual(headers.get(b'Connection'), b'close')
158+
self.assertEqual(headers.get(b'Content-Type'),
159+
b'text/html; charset=UTF-8')
160+
161+
self.assertFalse(m.on_body.called)
162+
p.feed_data(bytearray(RESPONSE1_BODY))
163+
m.on_body.assert_called_once_with(RESPONSE1_BODY)
164+
165+
m.on_message_complete.assert_called_once_with()
166+
167+
self.assertFalse(m.on_url.called)
168+
self.assertFalse(m.on_chunk_header.called)
169+
self.assertFalse(m.on_chunk_complete.called)
170+
112171
def test_parser_response_2(self):
113172
with self.assertRaisesRegex(TypeError, 'a bytes-like object'):
114173
httptools.HttpResponseParser(None).feed_data('')

0 commit comments

Comments
 (0)