diff --git a/Lib/http/server.py b/Lib/http/server.py index ebc85052aecb900..99bcfd2a0410cc7 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -547,17 +547,22 @@ def send_response_only(self, code, message=None): message = '' if not hasattr(self, '_headers_buffer'): self._headers_buffer = [] - self._headers_buffer.append(("%s %d %s\r\n" % - (self.protocol_version, code, message)).encode( - 'latin-1', 'strict')) + line = "%s %d %s\r\n" % (self.protocol_version, code, message) + if '\r' in line[:-2] or '\n' in line[:-2]: + raise ValueError("CR and LF characters are not allowed " + "in the response reason phrase") + self._headers_buffer.append(line.encode('latin-1', 'strict')) def send_header(self, keyword, value, *, _is_extra=False): """Send a MIME header to the headers buffer.""" if self.request_version != 'HTTP/0.9': if not hasattr(self, '_headers_buffer'): self._headers_buffer = [] - self._headers_buffer.append( - ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict')) + line = "%s: %s\r\n" % (keyword, value) + if '\r' in line[:-2] or '\n' in line[:-2]: + raise ValueError("CR and LF characters are not allowed " + "in HTTP header names or values") + self._headers_buffer.append(line.encode('latin-1', 'strict')) if not hasattr(self, '_default_response_headers'): self._default_response_headers = [] if not _is_extra: diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index d4ae032610a91e2..477076609244009 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -1184,6 +1184,29 @@ def test_header_buffering_of_send_header(self): self.assertEqual(output.getData(), b'Foo: foo\r\nbar: bar\r\n\r\n') self.assertEqual(output.numWrites, 1) + def test_send_header_rejects_crlf(self): + handler = SocketlessRequestHandler() + handler.wfile = BytesIO() + handler.request_version = 'HTTP/1.1' + + for keyword, value in ( + ('Foo', 'bar\r\nSet-Cookie: injected=1'), + ('Foo', 'bar\nSet-Cookie: injected=1'), + ('Foo', 'bar\rSet-Cookie: injected=1'), + ('Foo\r\nEvil', 'bar'), + ): + with self.subTest(keyword=keyword, value=value): + with self.assertRaises(ValueError): + handler.send_header(keyword, value) + + def test_send_response_only_rejects_crlf(self): + handler = SocketlessRequestHandler() + handler.wfile = BytesIO() + handler.request_version = 'HTTP/1.1' + + with self.assertRaises(ValueError): + handler.send_response_only(200, 'OK\r\nX-Injected: yes') + def test_header_unbuffered_when_continue(self): def _readAndReseek(f): diff --git a/Misc/NEWS.d/next/Security/2026-05-31-12-00-00.gh-issue-150679.Hn3Wp9.rst b/Misc/NEWS.d/next/Security/2026-05-31-12-00-00.gh-issue-150679.Hn3Wp9.rst new file mode 100644 index 000000000000000..f90c3af2d9f3e24 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-05-31-12-00-00.gh-issue-150679.Hn3Wp9.rst @@ -0,0 +1,5 @@ +:meth:`~http.server.BaseHTTPRequestHandler.send_header` and +:meth:`~http.server.BaseHTTPRequestHandler.send_response_only` now raise +:exc:`ValueError` when a header name, header value or the response reason +phrase contains a carriage return or line feed, preventing response +splitting and header injection.