Skip to content

Commit 114803a

Browse files
authored
Merge commit from fork
Validate Chunked-Encoding chunk footer
2 parents 9462006 + dff7cc3 commit 114803a

File tree

2 files changed

+51
-26
lines changed

2 files changed

+51
-26
lines changed

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

+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)