Skip to content

Commit

Permalink
Fix unhandled exception for invalid Unicode in Python HTTP parser (#7716
Browse files Browse the repository at this point in the history
)

## What do these changes do?

Fixes an unhandled exception in the Python HTTP parser that causes
servers to 500 when they should 400 upon receiving a header with an
invalid Unicode sequence.

## Are there changes in behavior for the user?

Nope.

## Related issue number

Fixes #7715

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
(cherry picked from commit 5a499d0)
  • Loading branch information
kenballus authored and patchback[bot] committed Oct 17, 2023
1 parent bc25c89 commit 196eedd
Show file tree
Hide file tree
Showing 5 changed files with 34 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGES/7715.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix unhandled exception when Python HTTP parser encounters unpaired Unicode surrogates.
7 changes: 3 additions & 4 deletions aiohttp/http_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,9 @@ def __init__(

class InvalidHeader(BadHttpMessage):
def __init__(self, hdr: Union[bytes, str]) -> None:
if isinstance(hdr, bytes):
hdr = hdr.decode("utf-8", "surrogateescape")
super().__init__(f"Invalid HTTP Header: {hdr}")
self.hdr = hdr
hdr_s = hdr.decode(errors="backslashreplace") if isinstance(hdr, bytes) else hdr
super().__init__(f"Invalid HTTP header: {hdr!r}")
self.hdr = hdr_s
self.args = (hdr,)


Expand Down
4 changes: 3 additions & 1 deletion aiohttp/http_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,9 @@ def parse_message(self, lines: List[bytes]) -> RawRequestMessage:
url = URL(path, encoded=True)
if url.scheme == "":
# not absolute-form
raise InvalidURLError(line)
raise InvalidURLError(
path.encode(errors="surrogateescape").decode("latin1")
)

# read headers
(
Expand Down
8 changes: 4 additions & 4 deletions tests/test_http_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class TestInvalidHeader:
def test_ctor(self) -> None:
err = http_exceptions.InvalidHeader("X-Spam")
assert err.code == 400
assert err.message == "Invalid HTTP Header: X-Spam"
assert err.message == "Invalid HTTP header: 'X-Spam'"
assert err.headers is None

def test_pickle(self) -> None:
Expand All @@ -112,17 +112,17 @@ def test_pickle(self) -> None:
pickled = pickle.dumps(err, proto)
err2 = pickle.loads(pickled)
assert err2.code == 400
assert err2.message == "Invalid HTTP Header: X-Spam"
assert err2.message == "Invalid HTTP header: 'X-Spam'"
assert err2.headers is None
assert err2.foo == "bar"

def test_str(self) -> None:
err = http_exceptions.InvalidHeader(hdr="X-Spam")
assert str(err) == "400, message:\n Invalid HTTP Header: X-Spam"
assert str(err) == "400, message:\n Invalid HTTP header: 'X-Spam'"

def test_repr(self) -> None:
err = http_exceptions.InvalidHeader(hdr="X-Spam")
expected = "<InvalidHeader: 400, message='Invalid HTTP Header: X-Spam'>"
expected = "<InvalidHeader: 400, message=\"Invalid HTTP header: 'X-Spam'\">"
assert repr(err) == expected


Expand Down
24 changes: 23 additions & 1 deletion tests/test_http_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,23 @@ def test_bad_headers(parser: Any, hdr: str) -> None:
parser.feed_data(text)


def test_unpaired_surrogate_in_header_py(loop: Any, protocol: Any) -> None:
parser = HttpRequestParserPy(
protocol,
loop,
2**16,
max_line_size=8190,
max_field_size=8190,
)
text = b"POST / HTTP/1.1\r\n\xff\r\n\r\n"
message = None
try:
parser.feed_data(text)
except http_exceptions.InvalidHeader as e:
message = e.message.encode("utf-8")
assert message is not None


def test_content_length_transfer_encoding(parser: Any) -> None:
text = (
b"GET / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\nTransfer-Encoding: a\r\n\r\n"
Expand Down Expand Up @@ -741,11 +758,16 @@ def test_http_request_parser_bad_version_number(parser: Any) -> None:
parser.feed_data(b"GET /test HTTP/1.32\r\n\r\n")


def test_http_request_parser_bad_uri(parser: Any) -> None:
def test_http_request_parser_bad_ascii_uri(parser: Any) -> None:
with pytest.raises(http_exceptions.InvalidURLError):
parser.feed_data(b"GET ! HTTP/1.1\r\n\r\n")


def test_http_request_parser_bad_nonascii_uri(parser: Any) -> None:
with pytest.raises(http_exceptions.InvalidURLError):
parser.feed_data(b"GET \xff HTTP/1.1\r\n\r\n")


@pytest.mark.parametrize("size", [40965, 8191])
def test_http_request_max_status_line(parser, size) -> None:
path = b"t" * (size - 5)
Expand Down

0 comments on commit 196eedd

Please sign in to comment.