aio-libs/aiohttp · critical · ValueError
Forbidden control character detected in headers. Potential h
Error message
Forbidden control character detected in headers. Potential header injection attack.
What it means
ValueError raised by _safe_header when a status line, header name, or header value contains a forbidden control character (0x00-0x08, 0x0a-0x1f, 0x7f). This guards against header injection / response splitting before serializing headers to the wire.
Source
Thrown at aiohttp/http_writer.py:374
The intended use is to write
await w.write(data)
await w.drain()
"""
protocol = self._protocol
if protocol.transport is not None and protocol._paused:
await protocol._drain_helper()
# https://www.rfc-editor.org/info/rfc9110/#section-5.5-5
# https://www.rfc-editor.org/info/rfc9112/#section-4-3
_FORBIDDEN_HEADER_CHARS_RE = re.compile(r"[\x00-\x08\x0a-\x1f\x7f]")
def _safe_header(string: str) -> str:
if _FORBIDDEN_HEADER_CHARS_RE.search(string) is not None:
raise ValueError(
"Forbidden control character detected in headers. "
"Potential header injection attack."
)
return string
def _py_serialize_headers(status_line: str, headers: "CIMultiDict[str]") -> bytes:
_safe_header(status_line)
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
try:
import aiohttp._http_writer as _http_writer # type: ignore[import-not-found]
View on GitHub (pinned to c0ef574e29)
Solutions
- Sanitise/strip control characters from any value placed into a header (encode filenames, strip CR/LF).
- Use urllib.parse.quote for filenames in Content-Disposition.
- Never concatenate raw user input into the status line.
- Reject or percent-encode header values at the trust boundary.
Example fix
// before
# filename = request.query['name'] # may contain \r\n
# headers = {'Content-Disposition': f'attachment; filename={filename}'}
# after
import re
from urllib.parse import quote
_FORBIDDEN = re.compile(r'[\x00-\x08\x0a-\x1f\x7f]')
safe = _FORBIDDEN.sub('', filename)
headers = {'Content-Disposition': f"attachment; filename=\"{quote(safe)}\""} Defensive patterns
Strategy: validation
Validate before calling
import re
_FORBIDDEN = re.compile(r'[\x00-\x08\x0a-\x1f\x7f]')
def safe_header(value: str) -> str:
if _FORBIDDEN.search(value) is not None:
raise ValueError('control char in header')
return value
# validate before setting
safe_header(location_value)
headers['Location'] = location_value Type guard
import re
_FORBIDDEN = re.compile(r'[\x00-\x08\x0a-\x1f\x7f]')
def is_safe_header(value: str) -> bool:
return _FORBIDDEN.search(value) is None Try / catch
try:
resp = web.Response(headers=headers)
except ValueError:
# header contained control char — sanitize first
... Prevention
- Never put raw user input in headers.
- Quote/strip CR/LF/NUL from filenames and redirect targets.
- Validate at the trust boundary.
When it happens
Trigger: Calling code sets a header or status line containing a CR/LF/NUL or other control char (e.g. embedding user input with a newline into a Location or Set-Cookie value). _py_serialize_headers runs _safe_header on every part and raises ValueError before any bytes go out.
Common situations: Putting unescaped user input into response headers (Location, Content-Disposition filenames); CRLF injection from a proxy; newline in a cookie value; copying a filename with control chars; header smuggling attempts.
Related errors
- Duplicate '{name}' header found.
- Transfer-Encoding can't be present with Content-Length
- Method cannot contain non-token characters {method!r} (found
- Invalid Content-Length header: {content_length_hdr!r}
- compress can not be set if Content-Encoding header is set
AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04).
Data as JSON: /data/errors/8ea35fbd6392e1cd.json.
Report an issue: GitHub.