Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit dff7cc3

Browse filesBrowse files
committed
Validate Chunked-Encoding chunk footer
Also add a bit more thoroughness to some tests that I noticed while I was working on it. Thanks to Jeppe Bonde Weikop for the report.
1 parent 31e626c commit dff7cc3
Copy full SHA for dff7cc3

File tree

2 files changed

+51
-26
lines changed
Filter options

2 files changed

+51
-26
lines changed

‎h11/_readers.py

Copy file name to clipboardExpand all lines: h11/_readers.py
+13-10
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,9 @@ def read_eof(self) -> NoReturn:
148148
class ChunkedReader:
149149
def __init__(self) -> None:
150150
self._bytes_in_chunk = 0
151-
# After reading a chunk, we have to throw away the trailing \r\n; if
152-
# this is >0 then we discard that many bytes before resuming regular
153-
# de-chunkification.
154-
self._bytes_to_discard = 0
151+
# After reading a chunk, we have to throw away the trailing \r\n.
152+
# This tracks the bytes that we need to match and throw away.
153+
self._bytes_to_discard = b""
155154
self._reading_trailer = False
156155

157156
def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
@@ -160,15 +159,19 @@ def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
160159
if lines is None:
161160
return None
162161
return EndOfMessage(headers=list(_decode_header_lines(lines)))
163-
if self._bytes_to_discard > 0:
164-
data = buf.maybe_extract_at_most(self._bytes_to_discard)
162+
if self._bytes_to_discard:
163+
data = buf.maybe_extract_at_most(len(self._bytes_to_discard))
165164
if data is None:
166165
return None
167-
self._bytes_to_discard -= len(data)
168-
if self._bytes_to_discard > 0:
166+
if data != self._bytes_to_discard[:len(data)]:
167+
raise LocalProtocolError(
168+
f"malformed chunk footer: {data!r} (expected {self._bytes_to_discard!r})"
169+
)
170+
self._bytes_to_discard = self._bytes_to_discard[len(data):]
171+
if self._bytes_to_discard:
169172
return None
170173
# else, fall through and read some more
171-
assert self._bytes_to_discard == 0
174+
assert self._bytes_to_discard == b""
172175
if self._bytes_in_chunk == 0:
173176
# We need to refill our chunk count
174177
chunk_header = buf.maybe_extract_next_line()
@@ -194,7 +197,7 @@ def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
194197
return None
195198
self._bytes_in_chunk -= len(data)
196199
if self._bytes_in_chunk == 0:
197-
self._bytes_to_discard = 2
200+
self._bytes_to_discard = b"\r\n"
198201
chunk_end = True
199202
else:
200203
chunk_end = False

‎h11/tests/test_io.py

Copy file name to clipboardExpand all lines: h11/tests/test_io.py
+38-16
Original file line numberDiff line numberDiff line change
@@ -348,22 +348,34 @@ def _run_reader(*args: Any) -> List[Event]:
348348
return normalize_data_events(events)
349349

350350

351-
def t_body_reader(thunk: Any, data: bytes, expected: Any, do_eof: bool = False) -> None:
351+
def t_body_reader(thunk: Any, data: bytes, expected: list, do_eof: bool = False) -> None:
352352
# Simple: consume whole thing
353353
print("Test 1")
354354
buf = makebuf(data)
355-
assert _run_reader(thunk(), buf, do_eof) == expected
355+
try:
356+
assert _run_reader(thunk(), buf, do_eof) == expected
357+
except LocalProtocolError:
358+
if LocalProtocolError in expected:
359+
pass
360+
else:
361+
raise
356362

357363
# Incrementally growing buffer
358364
print("Test 2")
359365
reader = thunk()
360366
buf = ReceiveBuffer()
361367
events = []
362-
for i in range(len(data)):
363-
events += _run_reader(reader, buf, False)
364-
buf += data[i : i + 1]
365-
events += _run_reader(reader, buf, do_eof)
366-
assert normalize_data_events(events) == expected
368+
try:
369+
for i in range(len(data)):
370+
events += _run_reader(reader, buf, False)
371+
buf += data[i : i + 1]
372+
events += _run_reader(reader, buf, do_eof)
373+
assert normalize_data_events(events) == expected
374+
except LocalProtocolError:
375+
if LocalProtocolError in expected:
376+
pass
377+
else:
378+
raise
367379

368380
is_complete = any(type(event) is EndOfMessage for event in expected)
369381
if is_complete and not do_eof:
@@ -424,14 +436,12 @@ def test_ChunkedReader() -> None:
424436
)
425437

426438
# refuses arbitrarily long chunk integers
427-
with pytest.raises(LocalProtocolError):
428-
# Technically this is legal HTTP/1.1, but we refuse to process chunk
429-
# sizes that don't fit into 20 characters of hex
430-
t_body_reader(ChunkedReader, b"9" * 100 + b"\r\nxxx", [Data(data=b"xxx")])
439+
# Technically this is legal HTTP/1.1, but we refuse to process chunk
440+
# sizes that don't fit into 20 characters of hex
441+
t_body_reader(ChunkedReader, b"9" * 100 + b"\r\nxxx", [LocalProtocolError])
431442

432443
# refuses garbage in the chunk count
433-
with pytest.raises(LocalProtocolError):
434-
t_body_reader(ChunkedReader, b"10\x00\r\nxxx", None)
444+
t_body_reader(ChunkedReader, b"10\x00\r\nxxx", [LocalProtocolError])
435445

436446
# handles (and discards) "chunk extensions" omg wtf
437447
t_body_reader(
@@ -445,10 +455,22 @@ def test_ChunkedReader() -> None:
445455

446456
t_body_reader(
447457
ChunkedReader,
448-
b"5 \r\n01234\r\n" + b"0\r\n\r\n",
458+
b"5 \t \r\n01234\r\n" + b"0\r\n\r\n",
449459
[Data(data=b"01234"), EndOfMessage()],
450460
)
451461

462+
# Chunked encoding with bad chunk termination characters are refused. Originally we
463+
# simply dropped the 2 bytes after a chunk, instead of validating that the bytes
464+
# were \r\n -- so we would successfully decode the data below as b"xxxa". And
465+
# apparently there are other HTTP processors that ignore the chunk length and just
466+
# keep reading until they see \r\n, so they would decode it as b"xxx__1a". Any time
467+
# two HTTP processors accept the same input but interpret it differently, there's a
468+
# possibility of request smuggling shenanigans. So we now reject this.
469+
t_body_reader(ChunkedReader, b"3\r\nxxx__1a\r\n", [LocalProtocolError])
470+
471+
# Confirm we check both bytes individually
472+
t_body_reader(ChunkedReader, b"3\r\nxxx\r_1a\r\n", [LocalProtocolError])
473+
t_body_reader(ChunkedReader, b"3\r\nxxx_\n1a\r\n", [LocalProtocolError])
452474

453475
def test_ContentLengthWriter() -> None:
454476
w = ContentLengthWriter(5)
@@ -471,8 +493,8 @@ def test_ContentLengthWriter() -> None:
471493
dowrite(w, EndOfMessage())
472494

473495
w = ContentLengthWriter(5)
474-
dowrite(w, Data(data=b"123")) == b"123"
475-
dowrite(w, Data(data=b"45")) == b"45"
496+
assert dowrite(w, Data(data=b"123")) == b"123"
497+
assert dowrite(w, Data(data=b"45")) == b"45"
476498
with pytest.raises(LocalProtocolError):
477499
dowrite(w, EndOfMessage(headers=[("Etag", "asdf")]))
478500

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.