Skip to content

Commit 7786f09

Browse files
authored
Merge pull request #3386 from bdarnell/curl-crlf
curl_httpclient,http1connection: Prohibit CR and LF in headers
2 parents 0efa9a4 + b0ffc58 commit 7786f09

File tree

3 files changed

+32
-10
lines changed

3 files changed

+32
-10
lines changed

tornado/curl_httpclient.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import functools
2020
import logging
2121
import pycurl
22+
import re
2223
import threading
2324
import time
2425
from io import BytesIO
@@ -44,6 +45,8 @@
4445

4546
curl_log = logging.getLogger("tornado.curl_httpclient")
4647

48+
CR_OR_LF_RE = re.compile(b"\r|\n")
49+
4750

4851
class CurlAsyncHTTPClient(AsyncHTTPClient):
4952
def initialize( # type: ignore
@@ -347,14 +350,15 @@ def _curl_setup_request(
347350
if "Pragma" not in request.headers:
348351
request.headers["Pragma"] = ""
349352

350-
curl.setopt(
351-
pycurl.HTTPHEADER,
352-
[
353-
b"%s: %s"
354-
% (native_str(k).encode("ASCII"), native_str(v).encode("ISO8859-1"))
355-
for k, v in request.headers.get_all()
356-
],
357-
)
353+
encoded_headers = [
354+
b"%s: %s"
355+
% (native_str(k).encode("ASCII"), native_str(v).encode("ISO8859-1"))
356+
for k, v in request.headers.get_all()
357+
]
358+
for line in encoded_headers:
359+
if CR_OR_LF_RE.search(line):
360+
raise ValueError("Illegal characters in header (CR or LF): %r" % line)
361+
curl.setopt(pycurl.HTTPHEADER, encoded_headers)
358362

359363
curl.setopt(
360364
pycurl.HEADERFUNCTION,

tornado/http1connection.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838

3939
from typing import cast, Optional, Type, Awaitable, Callable, Union, Tuple
4040

41+
CR_OR_LF_RE = re.compile(b"\r|\n")
42+
4143

4244
class _QuietException(Exception):
4345
def __init__(self) -> None:
@@ -453,8 +455,8 @@ def write_headers(
453455
)
454456
lines.extend(line.encode("latin1") for line in header_lines)
455457
for line in lines:
456-
if b"\n" in line:
457-
raise ValueError("Newline in header: " + repr(line))
458+
if CR_OR_LF_RE.search(line):
459+
raise ValueError("Illegal characters (CR or LF) in header: %r" % line)
458460
future = None
459461
if self.stream.closed():
460462
future = self._write_future = Future()

tornado/test/httpclient_test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,22 @@ def test_error_after_cancel(self):
725725
if el.logged_stack:
726726
break
727727

728+
def test_header_crlf(self):
729+
# Ensure that the client doesn't allow CRLF injection in headers. RFC 9112 section 2.2
730+
# prohibits a bare CR specifically and "a recipient MAY recognize a single LF as a line
731+
# terminator" so we check each character separately as well as the (redundant) CRLF pair.
732+
for header, name in [
733+
("foo\rbar:", "cr"),
734+
("foo\nbar:", "lf"),
735+
("foo\r\nbar:", "crlf"),
736+
]:
737+
with self.subTest(name=name, position="value"):
738+
with self.assertRaises(ValueError):
739+
self.fetch("/hello", headers={"foo": header})
740+
with self.subTest(name=name, position="key"):
741+
with self.assertRaises(ValueError):
742+
self.fetch("/hello", headers={header: "foo"})
743+
728744

729745
class RequestProxyTest(unittest.TestCase):
730746
def test_request_set(self):

0 commit comments

Comments
 (0)