{"record":{"id":"0c065654bab18b2f","repo":"aio-libs/aiohttp","slug":"reason-cannot-contain-r-or-n-0c0656","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_response.py","lineNumber":160,"sourceCode":"    def reason(self) -> str:\n        return self._reason\n\n    def set_status(\n        self,\n        status: int,\n        reason: str | None = None,\n    ) -> None:\n        assert (\n            not self.prepared\n        ), \"Cannot change the response status code after the headers have been sent\"\n        self._set_status(status, reason)\n\n    def _set_status(self, status: int, reason: str | None) -> None:\n        self._status = status\n        if reason is None:\n            reason = REASON_PHRASES.get(self._status, \"\")\n        elif \"\\r\" in reason or \"\\n\" in reason:\n            raise ValueError(\"Reason cannot contain \\\\r or \\\\n\")\n        self._reason = reason\n\n    @property\n    def keep_alive(self) -> bool | None:\n        return self._keep_alive\n\n    def force_close(self) -> None:\n        self._keep_alive = False\n\n    @property\n    def body_length(self) -> int:\n        return self._body_length\n\n    def enable_chunked_encoding(self) -> None:\n        \"\"\"Enables automatic chunked transfer encoding.\"\"\"\n        if hdrs.CONTENT_LENGTH in self._headers:\n            raise RuntimeError(\n                \"You can't enable chunked encoding when a content length is set\"","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/d041d4d0fd48c3f0832084d33be16cf1c4835f85/aiohttp/web_response.py#L142-L178","documentation":"StreamResponse._set_status (invoked by __init__ and set_status) rejects a reason containing \\r or \\n. The reason becomes the HTTP status-line reason phrase, so allowing CRLF would permit response splitting / header injection. This is the Response-side counterpart of the HTTPException guard.","triggerScenarios":"resp.set_status(200, 'OK\\nX-Inject: evil'); embedding str(exception) with newlines into the reason; templating user input into the status reason.","commonSituations":"Forwarding an error string verbatim as reason; multi-line text passed to set_status; copying a reason from upstream that contains CR.","solutions":["Keep reason a short static phrase; put detail in the response body.","Sanitize any dynamic reason: reason.replace('\\r','').replace('\\n','').","Validate reason with ^[^\\r\\n]*$ before setting it."],"exampleFix":"# before\nresp.set_status(200, str(upstream_err))\n# after\nresp.set_status(200, 'OK')\nresp.text = str(upstream_err)","handlingStrategy":"validation","validationCode":"import re\nif reason is not None:\n    reason = re.sub(r'[\\r\\n]+', ' ', reason)\nresp.set_status(status, 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":["Keep reason static; put detail in the body.","Sanitize any dynamic reason by stripping CR/LF.","Lint against CR/LF in reason literals."],"tags":["security","response-splitting","header-injection","response"],"backgroundTag":null,"analyzedSha":"d041d4d0fd48c3f0832084d33be16cf1c4835f85","analyzedAt":"2026-08-11T20:44:15.550Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}