{"record":{"id":"1254d5e9656baba8","repo":"aio-libs/aiohttp","slug":"reason-cannot-contain-r-or-n","errorCode":null,"errorMessage":"Reason cannot contain \\r or \\n","messagePattern":"Reason cannot contain \\\\r or \\\\n","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"aiohttp/web_exceptions.py","lineNumber":103,"sourceCode":"    # You should set in subclasses:\n    # status = 200\n\n    status_code = -1\n    empty_body = False\n    default_reason = \"\"  # Initialized at the end of the module\n\n    def __init__(\n        self,\n        *,\n        headers: LooseHeaders | None = None,\n        reason: str | None = None,\n        text: str | None = None,\n        content_type: str | None = None,\n    ) -> None:\n        if reason is None:\n            reason = self.default_reason\n        elif \"\\r\" in reason or \"\\n\" in reason:\n            raise ValueError(\"Reason cannot contain \\\\r or \\\\n\")\n\n        if text is None:\n            if not self.empty_body:\n                text = f\"{self.status_code}: {reason}\"\n        else:\n            if self.empty_body:\n                warnings.warn(\n                    f\"text argument is deprecated for HTTP status {self.status_code} \"\n                    \"since 4.0 and scheduled for removal in 5.0 (#3462),\"\n                    \"the response should be provided without a body\",\n                    DeprecationWarning,\n                    stacklevel=2,\n                )\n\n        if headers is not None:\n            real_headers = CIMultiDict(headers)\n        else:\n            real_headers = CIMultiDict()","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/d041d4d0fd48c3f0832084d33be16cf1c4835f85/aiohttp/web_exceptions.py#L85-L121","documentation":"HTTPException.__init__ (and all subclasses such as HTTPForbidden, HTTPNotFound) reject a reason containing \\r or \\n. The reason becomes the HTTP status-line reason phrase, so allowing CRLF would permit HTTP response splitting / header injection. The guard runs whenever an explicit reason is supplied.","triggerScenarios":"HTTPForbidden(reason='denied\\nX-Injected: evil'); embedding str(some_exception) (which contains newlines) into reason; templating user input into the reason phrase.","commonSituations":"Forwarding an upstream error message verbatim as reason; multi-line validation messages passed as reason; logging text reused for the status line.","solutions":["Keep reason a short static phrase; put detail in the text/body argument.","Sanitize any dynamic reason: reason = reason.replace('\\r','').replace('\\n','').","Validate reason with a regex (^[^\\r\\n]*$) before constructing the exception."],"exampleFix":"# before\nraise HTTPForbidden(reason=str(upstream_err))\n# after\nraise HTTPForbidden(text=str(upstream_err))","handlingStrategy":"validation","validationCode":"import re\nif reason is not None and re.search(r'[\\r\\n]', reason):\n    reason = re.sub(r'[\\r\\n]+', ' ', reason)\nraise web.HTTPForbidden(reason=reason)","typeGuard":"def is_safe_reason(r: str | None) -> TypeGuard[str]:\n    return r is not None and '\\r' not in r and '\\n' not in r","tryCatchPattern":null,"preventionTips":["Never put user input or exception text into the reason phrase.","Use the text= argument for detail, reason= for short static phrases.","Run a CI lint that rejects CR/LF in any literal reason string."],"tags":["security","response-splitting","header-injection","validation"],"backgroundTag":null,"analyzedSha":"d041d4d0fd48c3f0832084d33be16cf1c4835f85","analyzedAt":"2026-08-11T20:44:15.550Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}