Skip to content

Commit a1158c5

Browse files
Add validation of HTTP status line, header keys and values (#5452)
* 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 <[email protected]> * 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 <[email protected]>
1 parent 09aa9ce commit a1158c5

6 files changed

+42
-12
lines changed

CHANGES/4818.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add validation of HTTP header keys and values to prevent header injection.

CONTRIBUTORS.txt

+1
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ Felix Yan
115115
Fernanda Guimarães
116116
FichteFoll
117117
Florian Scheffler
118+
Franek Magiera
118119
Frederik Gladhorn
119120
Frederik Peter Aalund
120121
Gabriel Tremblay

aiohttp/_http_writer.pyx

+12
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ cdef str to_str(object s):
111111
return str(s)
112112

113113

114+
cdef void _safe_header(str string) except *:
115+
if "\r" in string or "\n" in string:
116+
raise ValueError(
117+
"Newline or carriage return character detected in HTTP status message or "
118+
"header. This is a potential security issue."
119+
)
120+
121+
114122
def _serialize_headers(str status_line, headers):
115123
cdef Writer writer
116124
cdef object key
@@ -119,6 +127,10 @@ def _serialize_headers(str status_line, headers):
119127

120128
_init_writer(&writer)
121129

130+
for key, val in headers.items():
131+
_safe_header(to_str(key))
132+
_safe_header(to_str(val))
133+
122134
try:
123135
if _write_str(&writer, status_line) < 0:
124136
raise

aiohttp/http_writer.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,19 @@ async def drain(self) -> None:
171171
await self._protocol._drain_helper()
172172

173173

174+
def _safe_header(string: str) -> str:
175+
if "\r" in string or "\n" in string:
176+
raise ValueError(
177+
"Newline or carriage return detected in headers. "
178+
"Potential header injection attack."
179+
)
180+
return string
181+
182+
174183
def _py_serialize_headers(status_line: str, headers: "CIMultiDict[str]") -> bytes:
175-
line = (
176-
status_line
177-
+ "\r\n"
178-
+ "".join([k + ": " + v + "\r\n" for k, v in headers.items()])
179-
)
180-
return line.encode("utf-8") + b"\r\n"
184+
headers_gen = (_safe_header(k) + ": " + _safe_header(v) for k, v in headers.items())
185+
line = status_line + "\r\n" + "\r\n".join(headers_gen) + "\r\n\r\n"
186+
return line.encode("utf-8")
181187

182188

183189
_serialize_headers = _py_serialize_headers

tests/test_http_writer.py

+14
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from unittest import mock
66

77
import pytest
8+
from multidict import CIMultiDict
89

910
from aiohttp import http
1011
from aiohttp.test_utils import make_mocked_coro
@@ -272,3 +273,16 @@ async def test_drain_no_transport(protocol: Any, transport: Any, loop: Any) -> N
272273
msg._protocol.transport = None
273274
await msg.drain()
274275
assert not protocol._drain_helper.called
276+
277+
278+
async def test_write_headers_prevents_injection(
279+
protocol: Any, transport: Any, loop: Any
280+
) -> None:
281+
msg = http.StreamWriter(protocol, loop)
282+
status_line = "HTTP/1.1 200 OK"
283+
wrong_headers = CIMultiDict({"Set-Cookie: abc=123\r\nContent-Length": "256"})
284+
with pytest.raises(ValueError):
285+
await msg.write_headers(status_line, wrong_headers)
286+
wrong_headers = CIMultiDict({"Content-Length": "256\r\nSet-Cookie: abc=123"})
287+
with pytest.raises(ValueError):
288+
await msg.write_headers(status_line, wrong_headers)

tests/test_web_response.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from aiohttp import HttpVersion, HttpVersion10, HttpVersion11, hdrs
1717
from aiohttp.helpers import ETag
18+
from aiohttp.http_writer import _serialize_headers
1819
from aiohttp.payload import BytesPayload
1920
from aiohttp.test_utils import make_mocked_coro, make_mocked_request
2021
from aiohttp.web import ContentCoding, Response, StreamResponse, json_response
@@ -59,12 +60,7 @@ def write(chunk):
5960
buf.extend(chunk)
6061

6162
async def write_headers(status_line, headers):
62-
headers = (
63-
status_line
64-
+ "\r\n"
65-
+ "".join([k + ": " + v + "\r\n" for k, v in headers.items()])
66-
)
67-
headers = headers.encode("utf-8") + b"\r\n"
63+
headers = _serialize_headers(status_line, headers)
6864
buf.extend(headers)
6965

7066
async def write_eof(chunk=b""):

0 commit comments

Comments
 (0)