Skip to content

Commit e36f345

Browse files
authored
Add valdation for header name (#6154)
1 parent 60865f2 commit e36f345

File tree

3 files changed

+72
-62
lines changed

3 files changed

+72
-62
lines changed

requests/_internal_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,20 @@
55
Provides utility functions that are consumed internally by Requests
66
which depend on extremely few external helpers (such as compat)
77
"""
8+
import re
89

910
from .compat import builtin_str
1011

12+
_VALID_HEADER_NAME_RE_BYTE = re.compile(rb"^[^:\s][^:\r\n]*$")
13+
_VALID_HEADER_NAME_RE_STR = re.compile(r"^[^:\s][^:\r\n]*$")
14+
_VALID_HEADER_VALUE_RE_BYTE = re.compile(rb"^\S[^\r\n]*$|^$")
15+
_VALID_HEADER_VALUE_RE_STR = re.compile(r"^\S[^\r\n]*$|^$")
16+
17+
HEADER_VALIDATORS = {
18+
bytes: (_VALID_HEADER_NAME_RE_BYTE, _VALID_HEADER_VALUE_RE_BYTE),
19+
str: (_VALID_HEADER_NAME_RE_STR, _VALID_HEADER_VALUE_RE_STR),
20+
}
21+
1122

1223
def to_native_string(string, encoding="ascii"):
1324
"""Given a string object, regardless of type, returns a representation of

requests/utils.py

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from .__version__ import __version__
2626

2727
# to_native_string is unused here, but imported here for backwards compatibility
28-
from ._internal_utils import to_native_string # noqa: F401
28+
from ._internal_utils import HEADER_VALIDATORS, to_native_string # noqa: F401
2929
from .compat import (
3030
Mapping,
3131
basestring,
@@ -1024,33 +1024,30 @@ def get_auth_from_url(url):
10241024
return auth
10251025

10261026

1027-
# Moved outside of function to avoid recompile every call
1028-
_CLEAN_HEADER_REGEX_BYTE = re.compile(b"^\\S[^\\r\\n]*$|^$")
1029-
_CLEAN_HEADER_REGEX_STR = re.compile(r"^\S[^\r\n]*$|^$")
1030-
1031-
10321027
def check_header_validity(header):
1033-
"""Verifies that header value is a string which doesn't contain
1034-
leading whitespace or return characters. This prevents unintended
1035-
header injection.
1028+
"""Verifies that header parts don't contain leading whitespace
1029+
reserved characters, or return characters.
10361030
10371031
:param header: tuple, in the format (name, value).
10381032
"""
10391033
name, value = header
10401034

1041-
if isinstance(value, bytes):
1042-
pat = _CLEAN_HEADER_REGEX_BYTE
1043-
else:
1044-
pat = _CLEAN_HEADER_REGEX_STR
1045-
try:
1046-
if not pat.match(value):
1035+
for part in header:
1036+
if type(part) not in HEADER_VALIDATORS:
10471037
raise InvalidHeader(
1048-
f"Invalid return character or leading space in header: {name}"
1038+
f"Header part ({part!r}) from {{{name!r}: {value!r}}} must be "
1039+
f"of type str or bytes, not {type(part)}"
10491040
)
1050-
except TypeError:
1041+
1042+
_validate_header_part(name, "name", HEADER_VALIDATORS[type(name)][0])
1043+
_validate_header_part(value, "value", HEADER_VALIDATORS[type(value)][1])
1044+
1045+
1046+
def _validate_header_part(header_part, header_kind, validator):
1047+
if not validator.match(header_part):
10511048
raise InvalidHeader(
1052-
f"Value for header {{{name}: {value}}} must be of type "
1053-
f"str or bytes, not {type(value)}"
1049+
f"Invalid leading whitespace, reserved character(s), or return"
1050+
f"character(s) in header {header_kind}: {header_part!r}"
10541051
)
10551052

10561053

tests/test_requests.py

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,7 +1096,7 @@ def test_non_prepared_request_error(self):
10961096
def test_custom_content_type(self, httpbin):
10971097
with open(__file__, "rb") as f1:
10981098
with open(__file__, "rb") as f2:
1099-
data={"stuff": json.dumps({"a": 123})}
1099+
data = {"stuff": json.dumps({"a": 123})}
11001100
files = {
11011101
"file1": ("test_requests.py", f1),
11021102
"file2": ("test_requests", f2, "text/py-content-type"),
@@ -1682,68 +1682,70 @@ def test_header_keys_are_native(self, httpbin):
16821682

16831683
def test_header_validation(self, httpbin):
16841684
"""Ensure prepare_headers regex isn't flagging valid header contents."""
1685-
headers_ok = {
1685+
valid_headers = {
16861686
"foo": "bar baz qux",
16871687
"bar": b"fbbq",
16881688
"baz": "",
16891689
"qux": "1",
16901690
}
1691-
r = requests.get(httpbin("get"), headers=headers_ok)
1692-
assert r.request.headers["foo"] == headers_ok["foo"]
1691+
r = requests.get(httpbin("get"), headers=valid_headers)
1692+
for key in valid_headers.keys():
1693+
valid_headers[key] == r.request.headers[key]
16931694

1694-
def test_header_value_not_str(self, httpbin):
1695+
@pytest.mark.parametrize(
1696+
"invalid_header, key",
1697+
(
1698+
({"foo": 3}, "foo"),
1699+
({"bar": {"foo": "bar"}}, "bar"),
1700+
({"baz": ["foo", "bar"]}, "baz"),
1701+
),
1702+
)
1703+
def test_header_value_not_str(self, httpbin, invalid_header, key):
16951704
"""Ensure the header value is of type string or bytes as
16961705
per discussion in GH issue #3386
16971706
"""
1698-
headers_int = {"foo": 3}
1699-
headers_dict = {"bar": {"foo": "bar"}}
1700-
headers_list = {"baz": ["foo", "bar"]}
1701-
1702-
# Test for int
1703-
with pytest.raises(InvalidHeader) as excinfo:
1704-
requests.get(httpbin("get"), headers=headers_int)
1705-
assert "foo" in str(excinfo.value)
1706-
# Test for dict
17071707
with pytest.raises(InvalidHeader) as excinfo:
1708-
requests.get(httpbin("get"), headers=headers_dict)
1709-
assert "bar" in str(excinfo.value)
1710-
# Test for list
1711-
with pytest.raises(InvalidHeader) as excinfo:
1712-
requests.get(httpbin("get"), headers=headers_list)
1713-
assert "baz" in str(excinfo.value)
1708+
requests.get(httpbin("get"), headers=invalid_header)
1709+
assert key in str(excinfo.value)
17141710

1715-
def test_header_no_return_chars(self, httpbin):
1711+
@pytest.mark.parametrize(
1712+
"invalid_header",
1713+
(
1714+
{"foo": "bar\r\nbaz: qux"},
1715+
{"foo": "bar\n\rbaz: qux"},
1716+
{"foo": "bar\nbaz: qux"},
1717+
{"foo": "bar\rbaz: qux"},
1718+
{"fo\ro": "bar"},
1719+
{"fo\r\no": "bar"},
1720+
{"fo\n\ro": "bar"},
1721+
{"fo\no": "bar"},
1722+
),
1723+
)
1724+
def test_header_no_return_chars(self, httpbin, invalid_header):
17161725
"""Ensure that a header containing return character sequences raise an
17171726
exception. Otherwise, multiple headers are created from single string.
17181727
"""
1719-
headers_ret = {"foo": "bar\r\nbaz: qux"}
1720-
headers_lf = {"foo": "bar\nbaz: qux"}
1721-
headers_cr = {"foo": "bar\rbaz: qux"}
1722-
1723-
# Test for newline
1724-
with pytest.raises(InvalidHeader):
1725-
requests.get(httpbin("get"), headers=headers_ret)
1726-
# Test for line feed
1727-
with pytest.raises(InvalidHeader):
1728-
requests.get(httpbin("get"), headers=headers_lf)
1729-
# Test for carriage return
17301728
with pytest.raises(InvalidHeader):
1731-
requests.get(httpbin("get"), headers=headers_cr)
1729+
requests.get(httpbin("get"), headers=invalid_header)
17321730

1733-
def test_header_no_leading_space(self, httpbin):
1731+
@pytest.mark.parametrize(
1732+
"invalid_header",
1733+
(
1734+
{" foo": "bar"},
1735+
{"\tfoo": "bar"},
1736+
{" foo": "bar"},
1737+
{"foo": " bar"},
1738+
{"foo": " bar"},
1739+
{"foo": "\tbar"},
1740+
{" ": "bar"},
1741+
),
1742+
)
1743+
def test_header_no_leading_space(self, httpbin, invalid_header):
17341744
"""Ensure headers containing leading whitespace raise
17351745
InvalidHeader Error before sending.
17361746
"""
1737-
headers_space = {"foo": " bar"}
1738-
headers_tab = {"foo": " bar"}
1739-
1740-
# Test for whitespace
1741-
with pytest.raises(InvalidHeader):
1742-
requests.get(httpbin("get"), headers=headers_space)
1743-
1744-
# Test for tab
17451747
with pytest.raises(InvalidHeader):
1746-
requests.get(httpbin("get"), headers=headers_tab)
1748+
requests.get(httpbin("get"), headers=invalid_header)
17471749

17481750
@pytest.mark.parametrize("files", ("foo", b"foo", bytearray(b"foo")))
17491751
def test_can_send_objects_with_files(self, httpbin, files):

0 commit comments

Comments
 (0)