Escape brackets and backslash in httptools HEADER_RE regex (#2824)
* Fix broken HEADER_RE regex in httptools HTTP implementation
The character class in HEADER_RE has unescaped [ and ] which causes
the regex to be parsed incorrectly. The ] prematurely closes the
character class after ':', so the remaining characters '={} \t"'
are treated as a literal sequence rather than part of the class.
As a result the regex never matches any invalid header name character
and the validation at line 496 is completely non-functional.
This escapes the brackets and backslash properly inside the
character class so all RFC 7230 header name separators are caught.
* Add tests for invalid HTTP header name validation
* Add comment explaining why no 500 is sent on invalid header name
* Use backticks around response_started in comment
---------
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
This commit is contained in:
parent
59ec1de7a4
commit
1fa697651b
@ -306,6 +306,42 @@ async def test_header_value_allowed_characters(http_protocol_cls: type[HTTPProto
|
||||
assert b"Hello, world" in protocol.transport.buffer
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
[
|
||||
pytest.param("bad header", id="reject_space"),
|
||||
pytest.param("bad\x00header", id="reject_null"),
|
||||
pytest.param("bad(header", id="reject_open_paren"),
|
||||
pytest.param("bad)header", id="reject_close_paren"),
|
||||
pytest.param("bad<header", id="reject_less_than"),
|
||||
pytest.param("bad>header", id="reject_greater_than"),
|
||||
pytest.param("bad@header", id="reject_at"),
|
||||
pytest.param("bad,header", id="reject_comma"),
|
||||
pytest.param("bad;header", id="reject_semicolon"),
|
||||
pytest.param("bad:header", id="reject_colon"),
|
||||
pytest.param("bad[header", id="reject_open_bracket"),
|
||||
pytest.param("bad]header", id="reject_close_bracket"),
|
||||
pytest.param("bad{header", id="reject_open_brace"),
|
||||
pytest.param("bad}header", id="reject_close_brace"),
|
||||
pytest.param("bad=header", id="reject_equals"),
|
||||
pytest.param('bad"header', id="reject_double_quote"),
|
||||
pytest.param("bad\\header", id="reject_backslash"),
|
||||
pytest.param("bad\theader", id="reject_tab"),
|
||||
pytest.param("bad\x7fheader", id="reject_del"),
|
||||
],
|
||||
)
|
||||
async def test_invalid_header_name(http_protocol_cls: type[HTTPProtocol], name: str):
|
||||
app = Response("Hello, world", media_type="text/plain", headers={name: "value"})
|
||||
protocol = get_connected_protocol(app, http_protocol_cls)
|
||||
protocol.data_received(SIMPLE_GET_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
# No 500 is sent because `response_started` is set before header validation,
|
||||
# so the error handler just closes the connection.
|
||||
assert b"HTTP/1.1 500 Internal Server Error" not in protocol.transport.buffer
|
||||
assert name.encode() not in protocol.transport.buffer
|
||||
assert protocol.transport.is_closing()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", ["/", "/?foo", "/?foo=bar", "/?foo=bar&baz=1"])
|
||||
async def test_request_logging(path: str, http_protocol_cls: type[HTTPProtocol], caplog: pytest.LogCaptureFixture):
|
||||
get_request_with_query_string = b"\r\n".join(
|
||||
|
||||
@ -27,7 +27,7 @@ from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT,
|
||||
from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl
|
||||
from uvicorn.server import ServerState
|
||||
|
||||
HEADER_RE = re.compile(b'[\x00-\x1f\x7f()<>@,;:[]={} \t\\"]')
|
||||
HEADER_RE = re.compile(b'[\x00-\x1f\x7f()<>@,;:\\[\\]={} \t\\\\"]')
|
||||
HEADER_VALUE_RE = re.compile(b"[\x00-\x08\x0a-\x1f\x7f]")
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user