aio-libs/aiohttp · error · ValueError
Forbidden control character detected in headers. Potential…
Error message
Forbidden control character detected in headers. Potential header injection attack.
What it means
Raised as a plain ValueError by _safe_header when serializing headers/status-line if any forbidden control character (bytes 0x00-0x08, 0x0a-0x1f, 0x7f) is found. The regex _FORBIDDEN_HEADER_CHARS_RE enforces RFC 9110 §5.5 / RFC 9112 §4.3, preventing header-injection (CRLF injection / response splitting) attacks. Applied to both the status line and every header name/value.
Solutions
- Sanitize any user-controlled value before placing it in a header: strip/reject CR, LF, NUL, and other control bytes.
- Use URL-encoding for arbitrary data passed in headers.
- Validate header values against _FORBIDDEN_HEADER_CHARS_RE (or [\x20-\x7e] plus tab) before setting them.
- For redirects, build Location from a safelist or validated URL, never raw input.
- Keep the security check in _safe_header enabled; do not bypass it.
Example fix
# before
import re
async def handler(request):
name = request.query.get('name', '')
return web.Response(headers={'X-Name': name}) # CRLF in name -> ValueError
# after
import re
_FORBIDDEN = re.compile(r'[\x00-\x08\x0a-\x1f\x7f]')
async def handler(request):
name = _FORBIDDEN.sub('', request.query.get('name', ''))
return web.Response(headers={'X-Name': name}) Defensive patterns
Strategy: validation
Validate before calling
import re
_FORBIDDEN_HEADER_CHARS_RE = re.compile(r'[\x00-\x08\x0a-\x1f\x7f]')
def safe_header_value(value: str) -> str | None:
if _FORBIDDEN_HEADER_CHARS_RE.search(value) is None:
return value
return None # or: return _FORBIDDEN_HEADER_CHARS_RE.sub('', value)
def validate_headers(headers: dict[str, str]) -> dict[str, str]:
out = {}
for k, v in headers.items():
if _FORBIDDEN_HEADER_CHARS_RE.search(k) is not None:
raise ValueError(f'Forbidden chars in header name {k!r}')
cleaned = safe_header_value(v)
if cleaned is None:
raise ValueError(f'Forbidden chars in header value for {k!r}')
out[k] = cleaned
return out Try / catch
try:
resp = web.Response(headers={'X-Name': user_input})
return resp
except ValueError:
# ValueError from _safe_header during serialization
return web.Response(status=400, text='Invalid header input') Prevention
- Sanitize all user-controlled input before placing it in headers (strip CR/LF/NUL/control).
- URL-encode arbitrary data passed via headers.
- Build redirect Location from validated/safelisted URLs only.
- Never bypass _safe_header; treat the ValueError as a real attack signal.
When it happens
Trigger: Application code sets a header name or value (or a status line) containing a raw CR/LF/NUL or other control byte, e.g. including '\r\n' inside a cookie/Location/User-Agent value. _py_serialize_headers runs _safe_header on each component and raises before any bytes hit the wire.
Common situations: User-controlled input placed into a header without sanitization (reflected XSS / injection vector); a Location header built from a query param containing newlines; logging/correlation IDs containing control chars; a server-side header value computed from untrusted data; tests with literal newlines in headers.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Reason cannot contain \r or \n
- Bad HTTP method in status line
- Duplicate ' ' header found.
- Method cannot contain non-token characters
- Reason cannot contain \r or \n
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/8ea35fbd6392e1cd.
Report an issue: GitHub.
Appendix: 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 d041d4d0fd)