Skip to content

Commit 4599b87

Browse files
authored
Improve performance of serializing headers (#10625)
Improve performance of serializing headers by moving the check for `\r` and `\n` into the write loop instead of making a separate call to check each disallowed character in the Python string.
1 parent 77ad7d7 commit 4599b87

File tree

3 files changed

+47
-21
lines changed

3 files changed

+47
-21
lines changed

CHANGES/10625.misc.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved performance of serializing headers -- by :user:`bdraco`.

aiohttp/_http_writer.pyx

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -97,27 +97,34 @@ cdef inline int _write_str(Writer* writer, str s):
9797
return -1
9898

9999

100-
# --------------- _serialize_headers ----------------------
101-
102-
cdef str to_str(object s):
100+
cdef inline int _write_str_raise_on_nlcr(Writer* writer, object s):
101+
cdef Py_UCS4 ch
102+
cdef str out_str
103103
if type(s) is str:
104-
return <str>s
104+
out_str = <str>s
105105
elif type(s) is _istr:
106-
return PyObject_Str(s)
106+
out_str = PyObject_Str(s)
107107
elif not isinstance(s, str):
108108
raise TypeError("Cannot serialize non-str key {!r}".format(s))
109109
else:
110-
return str(s)
110+
out_str = str(s)
111+
112+
for ch in out_str:
113+
if ch == 0x0D or ch == 0x0A:
114+
raise ValueError(
115+
"Newline or carriage return detected in headers. "
116+
"Potential header injection attack."
117+
)
118+
if _write_utf8(writer, ch) < 0:
119+
return -1
111120

112121

122+
# --------------- _serialize_headers ----------------------
113123

114124
def _serialize_headers(str status_line, headers):
115125
cdef Writer writer
116126
cdef object key
117127
cdef object val
118-
cdef bytes ret
119-
cdef str key_str
120-
cdef str val_str
121128

122129
_init_writer(&writer)
123130

@@ -130,22 +137,13 @@ def _serialize_headers(str status_line, headers):
130137
raise
131138

132139
for key, val in headers.items():
133-
key_str = to_str(key)
134-
val_str = to_str(val)
135-
136-
if "\r" in key_str or "\n" in key_str or "\r" in val_str or "\n" in val_str:
137-
raise ValueError(
138-
"Newline or carriage return character detected in HTTP status message or "
139-
"header. This is a potential security issue."
140-
)
141-
142-
if _write_str(&writer, key_str) < 0:
140+
if _write_str_raise_on_nlcr(&writer, key) < 0:
143141
raise
144142
if _write_byte(&writer, b':') < 0:
145143
raise
146144
if _write_byte(&writer, b' ') < 0:
147145
raise
148-
if _write_str(&writer, val_str) < 0:
146+
if _write_str_raise_on_nlcr(&writer, val) < 0:
149147
raise
150148
if _write_byte(&writer, b'\r') < 0:
151149
raise

tests/test_http_writer.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
import pytest
99
from multidict import CIMultiDict
1010

11-
from aiohttp import ClientConnectionResetError, http
11+
from aiohttp import ClientConnectionResetError, hdrs, http
1212
from aiohttp.base_protocol import BaseProtocol
13+
from aiohttp.http_writer import _serialize_headers
1314
from aiohttp.test_utils import make_mocked_coro
1415

1516

@@ -603,3 +604,29 @@ async def test_set_eof_after_write_headers(
603604
msg.set_eof()
604605
await msg.write_eof()
605606
assert not transport.write.called
607+
608+
609+
@pytest.mark.parametrize(
610+
"char",
611+
[
612+
"\n",
613+
"\r",
614+
],
615+
)
616+
def test_serialize_headers_raises_on_new_line_or_carriage_return(char: str) -> None:
617+
"""Verify serialize_headers raises on cr or nl in the headers."""
618+
status_line = "HTTP/1.1 200 OK"
619+
headers = CIMultiDict(
620+
{
621+
hdrs.CONTENT_TYPE: f"text/plain{char}",
622+
}
623+
)
624+
625+
with pytest.raises(
626+
ValueError,
627+
match=(
628+
"Newline or carriage return detected in headers. "
629+
"Potential header injection attack."
630+
),
631+
):
632+
_serialize_headers(status_line, headers)

0 commit comments

Comments
 (0)