Skip to content

Commit bad4131

Browse files
Reject null bytes in headers (#12210)
Co-authored-by: vmfunc <celeste@linux.com>
1 parent 1851048 commit bad4131

File tree

5 files changed

+34
-8
lines changed

5 files changed

+34
-8
lines changed

aiohttp/_http_parser.pyx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,13 @@ cdef class HttpParser:
384384
name = find_header(self._raw_name)
385385
value = self._raw_value.decode('utf-8', 'surrogateescape')
386386

387+
# reject null bytes in header values - matches the Python parser
388+
# check at http_parser.py. llhttp in lenient mode doesn't reject
389+
# these itself, so we need to catch them here.
390+
# ref: RFC 9110 section 5.5 (CTL chars forbidden in field values)
391+
if "\x00" in value:
392+
raise InvalidHeader(self._raw_value)
393+
387394
self._headers.append((name, value))
388395
if len(self._headers) > self._max_headers:
389396
raise BadHttpMessage("Too many headers received")

aiohttp/_http_writer.pyx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,9 @@ cdef inline int _write_str_raise_on_nlcr(Writer* writer, object s):
111111
out_str = str(s)
112112

113113
for ch in out_str:
114-
if ch == 0x0D or ch == 0x0A:
114+
if ch in {0x0D, 0x0A, 0x00}:
115115
raise ValueError(
116-
"Newline or carriage return detected in headers. "
116+
"Newline, carriage return, or null byte detected in headers. "
117117
"Potential header injection attack."
118118
)
119119
if _write_utf8(writer, ch) < 0:

aiohttp/http_writer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,9 @@ async def drain(self) -> None:
364364

365365

366366
def _safe_header(string: str) -> str:
367-
if "\r" in string or "\n" in string:
367+
if "\r" in string or "\n" in string or "\x00" in string:
368368
raise ValueError(
369-
"Newline or carriage return detected in headers. "
369+
"Newline, carriage return, or null byte detected in headers. "
370370
"Potential header injection attack."
371371
)
372372
return string

tests/test_http_parser.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,6 +1233,13 @@ def test_http_response_parser_strict_headers(response: HttpResponseParser) -> No
12331233
response.feed_data(b"HTTP/1.1 200 test\r\nFoo: abc\x01def\r\n\r\n")
12341234

12351235

1236+
def test_http_response_parser_null_byte_in_header_value(
1237+
response: HttpResponseParser,
1238+
) -> None:
1239+
with pytest.raises(http_exceptions.InvalidHeader):
1240+
response.feed_data(b"HTTP/1.1 200 OK\r\nFoo: abc\x00def\r\n\r\n")
1241+
1242+
12361243
def test_http_response_parser_bad_crlf(response: HttpResponseParser) -> None:
12371244
"""Still a lot of dodgy servers sending bad requests like this."""
12381245
messages, upgrade, tail = response.feed_data(

tests/test_http_writer.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,10 +1061,22 @@ def test_serialize_headers_raises_on_new_line_or_carriage_return(char: str) -> N
10611061

10621062
with pytest.raises(
10631063
ValueError,
1064-
match=(
1065-
"Newline or carriage return detected in headers. "
1066-
"Potential header injection attack."
1067-
),
1064+
match="detected in headers",
1065+
):
1066+
_serialize_headers(status_line, headers)
1067+
1068+
1069+
def test_serialize_headers_raises_on_null_byte() -> None:
1070+
status_line = "HTTP/1.1 200 OK"
1071+
headers = CIMultiDict(
1072+
{
1073+
hdrs.CONTENT_TYPE: "text/plain\x00",
1074+
}
1075+
)
1076+
1077+
with pytest.raises(
1078+
ValueError,
1079+
match="null byte detected in headers",
10681080
):
10691081
_serialize_headers(status_line, headers)
10701082

0 commit comments

Comments
 (0)