Skip to content

Commit 7815f03

Browse files
Harden header name normalization strip/lower boundaries
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 6ab3707 commit 7815f03

2 files changed

Lines changed: 97 additions & 2 deletions

File tree

hyperbrowser/header_utils.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,17 @@ def normalize_headers(
5151
):
5252
if not isinstance(key, str) or not isinstance(value, str):
5353
raise HyperbrowserError(effective_pair_error_message)
54-
normalized_key = key.strip()
54+
try:
55+
normalized_key = key.strip()
56+
if not isinstance(normalized_key, str):
57+
raise TypeError("normalized header name must be a string")
58+
except HyperbrowserError:
59+
raise
60+
except Exception as exc:
61+
raise HyperbrowserError(
62+
"Failed to normalize header name",
63+
original_error=exc,
64+
) from exc
5565
if not normalized_key:
5666
raise HyperbrowserError("header names must not be empty")
5767
if len(normalized_key) > _MAX_HEADER_NAME_LENGTH:
@@ -74,7 +84,17 @@ def normalize_headers(
7484
for character in f"{normalized_key}{value}"
7585
):
7686
raise HyperbrowserError("headers must not contain control characters")
77-
canonical_header_name = normalized_key.lower()
87+
try:
88+
canonical_header_name = normalized_key.lower()
89+
if not isinstance(canonical_header_name, str):
90+
raise TypeError("canonical header name must be a string")
91+
except HyperbrowserError:
92+
raise
93+
except Exception as exc:
94+
raise HyperbrowserError(
95+
"Failed to normalize header name",
96+
original_error=exc,
97+
) from exc
7898
if canonical_header_name in seen_header_names:
7999
raise HyperbrowserError(
80100
"duplicate header names are not allowed after normalization"

tests/test_header_utils.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,30 @@ def items(self):
3636
return [self._broken_item]
3737

3838

39+
class _BrokenStripHeaderName(str):
40+
def strip(self, chars=None): # type: ignore[override]
41+
_ = chars
42+
raise RuntimeError("header strip exploded")
43+
44+
45+
class _BrokenLowerHeaderName(str):
46+
def strip(self, chars=None): # type: ignore[override]
47+
_ = chars
48+
return self
49+
50+
def lower(self): # type: ignore[override]
51+
raise RuntimeError("header lower exploded")
52+
53+
54+
class _NonStringLowerHeaderName(str):
55+
def strip(self, chars=None): # type: ignore[override]
56+
_ = chars
57+
return self
58+
59+
def lower(self): # type: ignore[override]
60+
return object()
61+
62+
3963
def test_normalize_headers_trims_header_names():
4064
headers = normalize_headers(
4165
{" X-Correlation-Id ": "abc123"},
@@ -53,6 +77,57 @@ def test_normalize_headers_rejects_empty_header_name():
5377
)
5478

5579

80+
def test_normalize_headers_wraps_header_name_strip_failures():
81+
with pytest.raises(
82+
HyperbrowserError, match="Failed to normalize header name"
83+
) as exc_info:
84+
normalize_headers(
85+
{_BrokenStripHeaderName("X-Trace-Id"): "trace-1"},
86+
mapping_error_message="headers must be a mapping of string pairs",
87+
)
88+
89+
assert exc_info.value.original_error is not None
90+
91+
92+
def test_normalize_headers_preserves_hyperbrowser_header_name_strip_failures():
93+
class _BrokenStripHeaderName(str):
94+
def strip(self, chars=None): # type: ignore[override]
95+
_ = chars
96+
raise HyperbrowserError("custom strip failure")
97+
98+
with pytest.raises(HyperbrowserError, match="custom strip failure") as exc_info:
99+
normalize_headers(
100+
{_BrokenStripHeaderName("X-Trace-Id"): "trace-1"},
101+
mapping_error_message="headers must be a mapping of string pairs",
102+
)
103+
104+
assert exc_info.value.original_error is None
105+
106+
107+
def test_normalize_headers_wraps_header_name_lower_failures():
108+
with pytest.raises(
109+
HyperbrowserError, match="Failed to normalize header name"
110+
) as exc_info:
111+
normalize_headers(
112+
{_BrokenLowerHeaderName("X-Trace-Id"): "trace-1"},
113+
mapping_error_message="headers must be a mapping of string pairs",
114+
)
115+
116+
assert exc_info.value.original_error is not None
117+
118+
119+
def test_normalize_headers_wraps_non_string_header_name_lower_results():
120+
with pytest.raises(
121+
HyperbrowserError, match="Failed to normalize header name"
122+
) as exc_info:
123+
normalize_headers(
124+
{_NonStringLowerHeaderName("X-Trace-Id"): "trace-1"},
125+
mapping_error_message="headers must be a mapping of string pairs",
126+
)
127+
128+
assert exc_info.value.original_error is not None
129+
130+
56131
def test_normalize_headers_rejects_overly_long_header_names():
57132
long_header_name = "X-" + ("a" * 255)
58133
with pytest.raises(

0 commit comments

Comments
 (0)