Skip to content

Commit

Permalink
Allow horizontal tabs \t in response header values (#2345)
Browse files Browse the repository at this point in the history
* Add test to check that response header splitting is prevented
* Allow horizontal tabs \t in response header values

---------

Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
  • Loading branch information
jhominal and Kludex committed May 28, 2024
1 parent 9a6b3a8 commit 6d666d9
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 1 deletion.
30 changes: 30 additions & 0 deletions tests/protocols/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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})
Expand Down
2 changes: 1 addition & 1 deletion uvicorn/protocols/http/httptools_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 6d666d9

Please sign in to comment.