Fix CVE-2025-43859

Signed-off-by: hdliu <dev03108@linx-info.com>
(cherry picked from commit d865de62eda8c36e91acd9fab085ee22b9c862b5)
This commit is contained in:
hdliu 2025-04-28 10:26:18 +08:00 committed by openeuler-sync-bot
parent d8987e9e82
commit f8322af435
2 changed files with 173 additions and 2 deletions

View File

@ -0,0 +1,166 @@
From ae3510465ceb3586b15bef050257215ef37dfb68 Mon Sep 17 00:00:00 2001
From: hdliu <dev03108@linx-info.com>
Date: Mon, 28 Apr 2025 10:07:30 +0800
Subject: [PATCH] Validate Chunked-Encoding chunk footer
Signed-off-by: hdliu <dev03108@linx-info.com>
---
h11/_readers.py | 23 ++++++++++--------
h11/tests/test_io.py | 56 ++++++++++++++++++++++++++++++--------------
2 files changed, 52 insertions(+), 27 deletions(-)
diff --git a/h11/_readers.py b/h11/_readers.py
index 08a9574..7db4dac 100644
--- a/h11/_readers.py
+++ b/h11/_readers.py
@@ -148,10 +148,9 @@ chunk_header_re = re.compile(chunk_header.encode("ascii"))
class ChunkedReader:
def __init__(self) -> None:
self._bytes_in_chunk = 0
- # After reading a chunk, we have to throw away the trailing \r\n; if
- # this is >0 then we discard that many bytes before resuming regular
- # de-chunkification.
- self._bytes_to_discard = 0
+ # After reading a chunk, we have to throw away the trailing \r\n.
+ # This tracks the bytes that we need to match and throw away.
+ self._bytes_to_discard = b""
self._reading_trailer = False
def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
@@ -160,15 +159,19 @@ class ChunkedReader:
if lines is None:
return None
return EndOfMessage(headers=list(_decode_header_lines(lines)))
- if self._bytes_to_discard > 0:
- data = buf.maybe_extract_at_most(self._bytes_to_discard)
+ if self._bytes_to_discard:
+ data = buf.maybe_extract_at_most(len(self._bytes_to_discard))
if data is None:
return None
- self._bytes_to_discard -= len(data)
- if self._bytes_to_discard > 0:
+ if data != self._bytes_to_discard[:len(data)]:
+ raise LocalProtocolError(
+ f"malformed chunk footer: {data!r} (expected {self._bytes_to_discard!r})"
+ )
+ self._bytes_to_discard = self._bytes_to_discard[len(data):]
+ if self._bytes_to_discard:
return None
# else, fall through and read some more
- assert self._bytes_to_discard == 0
+ assert self._bytes_to_discard == b""
if self._bytes_in_chunk == 0:
# We need to refill our chunk count
chunk_header = buf.maybe_extract_next_line()
@@ -194,7 +197,7 @@ class ChunkedReader:
return None
self._bytes_in_chunk -= len(data)
if self._bytes_in_chunk == 0:
- self._bytes_to_discard = 2
+ self._bytes_to_discard = b"\r\n"
chunk_end = True
else:
chunk_end = False
diff --git a/h11/tests/test_io.py b/h11/tests/test_io.py
index 2b47c0e..c759077 100644
--- a/h11/tests/test_io.py
+++ b/h11/tests/test_io.py
@@ -360,22 +360,34 @@ def _run_reader(*args: Any) -> List[Event]:
return normalize_data_events(events)
-def t_body_reader(thunk: Any, data: bytes, expected: Any, do_eof: bool = False) -> None:
+def t_body_reader(thunk: Any, data: bytes, expected: list, do_eof: bool = False) -> None:
# Simple: consume whole thing
print("Test 1")
buf = makebuf(data)
- assert _run_reader(thunk(), buf, do_eof) == expected
+ try:
+ assert _run_reader(thunk(), buf, do_eof) == expected
+ except LocalProtocolError:
+ if LocalProtocolError in expected:
+ pass
+ else:
+ raise
# Incrementally growing buffer
print("Test 2")
reader = thunk()
buf = ReceiveBuffer()
events = []
- for i in range(len(data)):
- events += _run_reader(reader, buf, False)
- buf += data[i : i + 1]
- events += _run_reader(reader, buf, do_eof)
- assert normalize_data_events(events) == expected
+ try:
+ for i in range(len(data)):
+ events += _run_reader(reader, buf, False)
+ buf += data[i : i + 1]
+ events += _run_reader(reader, buf, do_eof)
+ assert normalize_data_events(events) == expected
+ except LocalProtocolError:
+ if LocalProtocolError in expected:
+ pass
+ else:
+ raise
is_complete = any(type(event) is EndOfMessage for event in expected)
if is_complete and not do_eof:
@@ -436,14 +448,12 @@ def test_ChunkedReader() -> None:
)
# refuses arbitrarily long chunk integers
- with pytest.raises(LocalProtocolError):
- # Technically this is legal HTTP/1.1, but we refuse to process chunk
- # sizes that don't fit into 20 characters of hex
- t_body_reader(ChunkedReader, b"9" * 100 + b"\r\nxxx", [Data(data=b"xxx")])
+ # Technically this is legal HTTP/1.1, but we refuse to process chunk
+ # sizes that don't fit into 20 characters of hex
+ t_body_reader(ChunkedReader, b"9" * 100 + b"\r\nxxx", [LocalProtocolError])
# refuses garbage in the chunk count
- with pytest.raises(LocalProtocolError):
- t_body_reader(ChunkedReader, b"10\x00\r\nxxx", None)
+ t_body_reader(ChunkedReader, b"10\x00\r\nxxx", [LocalProtocolError])
# handles (and discards) "chunk extensions" omg wtf
t_body_reader(
@@ -457,10 +467,22 @@ def test_ChunkedReader() -> None:
t_body_reader(
ChunkedReader,
- b"5 \r\n01234\r\n" + b"0\r\n\r\n",
+ b"5 \t \r\n01234\r\n" + b"0\r\n\r\n",
[Data(data=b"01234"), EndOfMessage()],
)
+ # Chunked encoding with bad chunk termination characters are refused. Originally we
+ # simply dropped the 2 bytes after a chunk, instead of validating that the bytes
+ # were \r\n -- so we would successfully decode the data below as b"xxxa". And
+ # apparently there are other HTTP processors that ignore the chunk length and just
+ # keep reading until they see \r\n, so they would decode it as b"xxx__1a". Any time
+ # two HTTP processors accept the same input but interpret it differently, there's a
+ # possibility of request smuggling shenanigans. So we now reject this.
+ t_body_reader(ChunkedReader, b"3\r\nxxx__1a\r\n", [LocalProtocolError])
+
+ # Confirm we check both bytes individually
+ t_body_reader(ChunkedReader, b"3\r\nxxx\r_1a\r\n", [LocalProtocolError])
+ t_body_reader(ChunkedReader, b"3\r\nxxx_\n1a\r\n", [LocalProtocolError])
def test_ContentLengthWriter() -> None:
w = ContentLengthWriter(5)
@@ -483,8 +505,8 @@ def test_ContentLengthWriter() -> None:
dowrite(w, EndOfMessage())
w = ContentLengthWriter(5)
- dowrite(w, Data(data=b"123")) == b"123"
- dowrite(w, Data(data=b"45")) == b"45"
+ assert dowrite(w, Data(data=b"123")) == b"123"
+ assert dowrite(w, Data(data=b"45")) == b"45"
with pytest.raises(LocalProtocolError):
dowrite(w, EndOfMessage(headers=[("Etag", "asdf")]))
--
2.33.0

View File

@ -3,13 +3,15 @@
%global _empty_manifest_terminate_build 0
Name: python-h11
Version: 0.14.0
Release: 1
Release: 2
Summary: A pure-Python, bring-your-own-I/O implementation of HTTP/1.1
License: MIT
URL: https://github.com/python-hyper/h11
Source0: https://github.com/python-hyper/h11/archive/refs/tags/v%{version}.tar.gz#/h11-%{version}.tar.gz
BuildArch: noarch
Patch0001: backport-upstream_CVE-2025-43859.patch
%description
h11 is suitable for implementing both servers and clients, and has a
@ -38,7 +40,7 @@ pleasantly symmetric API: the events you send as a client are exactly
the ones that you receive as a server and vice-versa.
%prep
%autosetup -n h11-%{version}
%autosetup -n h11-%{version} -p1
%build
%py3_build
@ -83,6 +85,9 @@ py.test-%{python3_version} --verbose
%{_docdir}/*
%changelog
* Mon Apr 28 2025 hdliu <dev03108@linx-info.com> - 0.14.0-2
- Fix CVE-2025-43963
* Fri Sep 30 2022 jiangxinyu <jiangxinyu@kylinos.cn> - 0.14.0-1
- Upgrade to 0.14.0