Compare commits

...

4 Commits

Author SHA1 Message Date
Marcelo Trylesinski
798809cc6e Use backticks around response_started in comment 2026-03-15 14:54:07 +01:00
Marcelo Trylesinski
d01c83cd84 Add comment explaining why no 500 is sent on invalid header name 2026-03-15 14:53:36 +01:00
Marcelo Trylesinski
7b4e690296 Add tests for invalid HTTP header name validation 2026-03-15 14:50:55 +01:00
Kadir Can Ozden
1a5b184324 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.
2026-02-20 05:40:11 +03:00
2 changed files with 37 additions and 1 deletions

View File

@ -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(

View File

@ -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]")