Skip to content

Commit

Permalink
Add validation of HTTP status line, header keys and values (#5452) (#…
Browse files Browse the repository at this point in the history
…5984)

* Add validation of HTTP status line, header keys and values

* Apply review comments

* Rename _check_string to _safe_header and remove validation for the status_line

* Update aiohttp/http_writer.py

Co-authored-by: Sam Bull <aa6bs0@sambull.org>

* Modify changelog message

* Refactor headers join

* Refactor headers serialization back to the broken down version and add the second CRLF sign after the headers

* Update aiohttp/http_writer.py

Co-authored-by: Sam Bull <aa6bs0@sambull.org>
(cherry picked from commit a1158c5)

Co-authored-by: Franek Magiera <framagie@gmail.com>
  • Loading branch information
patchback[bot] and franekmagiera committed Sep 4, 2021
1 parent 02db846 commit 3cae27a
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGES/4818.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add validation of HTTP header keys and values to prevent header injection.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ Felix Yan
Fernanda Guimarães
FichteFoll
Florian Scheffler
Franek Magiera
Frederik Gladhorn
Frederik Peter Aalund
Gabriel Tremblay
Expand Down
12 changes: 12 additions & 0 deletions aiohttp/_http_writer.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ cdef str to_str(object s):
return str(s)


cdef void _safe_header(str string) except *:
if "\r" in string or "\n" in string:
raise ValueError(
"Newline or carriage return character detected in HTTP status message or "
"header. This is a potential security issue."
)


def _serialize_headers(str status_line, headers):
cdef Writer writer
cdef object key
Expand All @@ -119,6 +127,10 @@ def _serialize_headers(str status_line, headers):

_init_writer(&writer)

for key, val in headers.items():
_safe_header(to_str(key))
_safe_header(to_str(val))

try:
if _write_str(&writer, status_line) < 0:
raise
Expand Down
18 changes: 12 additions & 6 deletions aiohttp/http_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,19 @@ async def drain(self) -> None:
await self._protocol._drain_helper()


def _safe_header(string: str) -> str:
if "\r" in string or "\n" in string:
raise ValueError(
"Newline or carriage return detected in headers. "
"Potential header injection attack."
)
return string


def _py_serialize_headers(status_line: str, headers: "CIMultiDict[str]") -> bytes:
line = (
status_line
+ "\r\n"
+ "".join([k + ": " + v + "\r\n" for k, v in headers.items()])
)
return line.encode("utf-8") + b"\r\n"
headers_gen = (_safe_header(k) + ": " + _safe_header(v) for k, v in headers.items())
line = status_line + "\r\n" + "\r\n".join(headers_gen) + "\r\n\r\n"
return line.encode("utf-8")


_serialize_headers = _py_serialize_headers
Expand Down
12 changes: 12 additions & 0 deletions tests/test_http_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from unittest import mock

import pytest
from multidict import CIMultiDict

from aiohttp import http
from aiohttp.test_utils import make_mocked_coro
Expand Down Expand Up @@ -246,3 +247,14 @@ async def test_drain_no_transport(protocol, transport, loop) -> None:
msg._protocol.transport = None
await msg.drain()
assert not protocol._drain_helper.called


async def test_write_headers_prevents_injection(protocol, transport, loop) -> None:
msg = http.StreamWriter(protocol, loop)
status_line = "HTTP/1.1 200 OK"
wrong_headers = CIMultiDict({"Set-Cookie: abc=123\r\nContent-Length": "256"})
with pytest.raises(ValueError):
await msg.write_headers(status_line, wrong_headers)
wrong_headers = CIMultiDict({"Content-Length": "256\r\nSet-Cookie: abc=123"})
with pytest.raises(ValueError):
await msg.write_headers(status_line, wrong_headers)
8 changes: 2 additions & 6 deletions tests/test_web_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from aiohttp import HttpVersion, HttpVersion10, HttpVersion11, hdrs
from aiohttp.helpers import ETag
from aiohttp.http_writer import _serialize_headers
from aiohttp.payload import BytesPayload
from aiohttp.test_utils import make_mocked_coro, make_mocked_request
from aiohttp.web import ContentCoding, Response, StreamResponse, json_response
Expand Down Expand Up @@ -56,12 +57,7 @@ def write(chunk):
buf.extend(chunk)

async def write_headers(status_line, headers):
headers = (
status_line
+ "\r\n"
+ "".join([k + ": " + v + "\r\n" for k, v in headers.items()])
)
headers = headers.encode("utf-8") + b"\r\n"
headers = _serialize_headers(status_line, headers)
buf.extend(headers)

async def write_eof(chunk=b""):
Expand Down

0 comments on commit 3cae27a

Please sign in to comment.