diff --git a/tests/protocols/test_http.py b/tests/protocols/test_http.py index 7bb110e00..45ccc1d23 100644 --- a/tests/protocols/test_http.py +++ b/tests/protocols/test_http.py @@ -251,6 +251,25 @@ async def test_get_request(http_protocol_cls: HTTPProtocol): assert b"Hello, world" in protocol.transport.buffer +@pytest.mark.parametrize( + "char", + [ + pytest.param("c", id="allow_ascii_letter"), + pytest.param("\t", id="allow_tab"), + pytest.param(" ", id="allow_space"), + pytest.param("ยต", id="allow_non_ascii_char"), + ], +) +async def test_header_value_allowed_characters(http_protocol_cls: HTTPProtocol, char: str): + app = Response("Hello, world", media_type="text/plain", headers={"key": f"<{char}>"}) + protocol = get_connected_protocol(app, http_protocol_cls) + protocol.data_received(SIMPLE_GET_REQUEST) + await protocol.loop.run_one() + assert b"HTTP/1.1 200 OK" in protocol.transport.buffer + assert (b"\r\nkey: <" + char.encode() + b">\r\n") in protocol.transport.buffer + assert b"Hello, world" in protocol.transport.buffer + + @pytest.mark.parametrize("path", ["/", "/?foo", "/?foo=bar", "/?foo=bar&baz=1"]) async def test_request_logging(path: str, http_protocol_cls: HTTPProtocol, caplog: pytest.LogCaptureFixture): get_request_with_query_string = b"\r\n".join( @@ -492,6 +511,17 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable assert protocol.transport.is_closing() +async def test_response_header_splitting(http_protocol_cls: HTTPProtocol): + app = Response(b"", headers={"key": "value\r\nCookie: smuggled=value"}) + + protocol = get_connected_protocol(app, http_protocol_cls) + protocol.data_received(SIMPLE_GET_REQUEST) + await protocol.loop.run_one() + assert b"HTTP/1.1 500 Internal Server Error" not in protocol.transport.buffer + assert b"\r\nCookie: smuggled=value\r\n" not in protocol.transport.buffer + assert protocol.transport.is_closing() + + async def test_duplicate_start_message(http_protocol_cls: HTTPProtocol): async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): await send({"type": "http.response.start", "status": 200}) diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index e54966609..6dff0d631 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -26,7 +26,7 @@ from uvicorn.server import ServerState HEADER_RE = re.compile(b'[\x00-\x1f\x7f()<>@,;:[]={} \t\\"]') -HEADER_VALUE_RE = re.compile(b"[\x00-\x1f\x7f]") +HEADER_VALUE_RE = re.compile(b"[\x00-\x08\x0a-\x1f\x7f]") def _get_status_line(status_code: int) -> bytes: