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

  1. Sanitise/strip control characters from any value placed into a header (encode filenames, strip CR/LF).
  2. Use urllib.parse.quote for filenames in Content-Disposition.
  3. Never concatenate raw user input into the status line.
  4. 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

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


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/8ea35fbd6392e1cd.json. Report an issue: GitHub.