Skip to content

Commit 3b4d93d

Browse files
Sanitize header names and block newline injection
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 37220ef commit 3b4d93d

File tree

5 files changed

+91
-13
lines changed

5 files changed

+91
-13
lines changed

hyperbrowser/config.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,19 @@ def __post_init__(self) -> None:
3636
for key, value in self.headers.items():
3737
if not isinstance(key, str) or not isinstance(value, str):
3838
raise HyperbrowserError("headers must be a mapping of string pairs")
39-
normalized_headers[key] = value
39+
normalized_key = key.strip()
40+
if not normalized_key:
41+
raise HyperbrowserError("header names must not be empty")
42+
if (
43+
"\n" in normalized_key
44+
or "\r" in normalized_key
45+
or "\n" in value
46+
or "\r" in value
47+
):
48+
raise HyperbrowserError(
49+
"headers must not contain newline characters"
50+
)
51+
normalized_headers[normalized_key] = value
4052
self.headers = normalized_headers
4153

4254
@classmethod

hyperbrowser/transport/async_transport.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,24 @@ def __init__(self, api_key: str, headers: Optional[Mapping[str, str]] = None):
1717
"User-Agent": f"hyperbrowser-python-sdk/{__version__}",
1818
}
1919
if headers:
20-
if any(
21-
not isinstance(key, str) or not isinstance(value, str)
22-
for key, value in headers.items()
23-
):
24-
raise HyperbrowserError("headers must be a mapping of string pairs")
25-
merged_headers.update(headers)
20+
normalized_headers = {}
21+
for key, value in headers.items():
22+
if not isinstance(key, str) or not isinstance(value, str):
23+
raise HyperbrowserError("headers must be a mapping of string pairs")
24+
normalized_key = key.strip()
25+
if not normalized_key:
26+
raise HyperbrowserError("header names must not be empty")
27+
if (
28+
"\n" in normalized_key
29+
or "\r" in normalized_key
30+
or "\n" in value
31+
or "\r" in value
32+
):
33+
raise HyperbrowserError(
34+
"headers must not contain newline characters"
35+
)
36+
normalized_headers[normalized_key] = value
37+
merged_headers.update(normalized_headers)
2638
self.client = httpx.AsyncClient(headers=merged_headers)
2739
self._closed = False
2840

hyperbrowser/transport/sync.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,24 @@ def __init__(self, api_key: str, headers: Optional[Mapping[str, str]] = None):
1717
"User-Agent": f"hyperbrowser-python-sdk/{__version__}",
1818
}
1919
if headers:
20-
if any(
21-
not isinstance(key, str) or not isinstance(value, str)
22-
for key, value in headers.items()
23-
):
24-
raise HyperbrowserError("headers must be a mapping of string pairs")
25-
merged_headers.update(headers)
20+
normalized_headers = {}
21+
for key, value in headers.items():
22+
if not isinstance(key, str) or not isinstance(value, str):
23+
raise HyperbrowserError("headers must be a mapping of string pairs")
24+
normalized_key = key.strip()
25+
if not normalized_key:
26+
raise HyperbrowserError("header names must not be empty")
27+
if (
28+
"\n" in normalized_key
29+
or "\r" in normalized_key
30+
or "\n" in value
31+
or "\r" in value
32+
):
33+
raise HyperbrowserError(
34+
"headers must not contain newline characters"
35+
)
36+
normalized_headers[normalized_key] = value
37+
merged_headers.update(normalized_headers)
2638
self.client = httpx.Client(headers=merged_headers)
2739

2840
def _handle_response(self, response: httpx.Response) -> APIResponse:

tests/test_config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,24 @@ def test_client_config_rejects_non_string_header_pairs():
142142
ClientConfig(api_key="test-key", headers={"X-Correlation-Id": 123}) # type: ignore[dict-item]
143143

144144

145+
def test_client_config_rejects_empty_header_name():
146+
with pytest.raises(HyperbrowserError, match="header names must not be empty"):
147+
ClientConfig(api_key="test-key", headers={" ": "value"})
148+
149+
150+
def test_client_config_rejects_newline_header_values():
151+
with pytest.raises(
152+
HyperbrowserError, match="headers must not contain newline characters"
153+
):
154+
ClientConfig(api_key="test-key", headers={"X-Correlation-Id": "bad\nvalue"})
155+
156+
157+
def test_client_config_normalizes_header_name_whitespace():
158+
config = ClientConfig(api_key="test-key", headers={" X-Correlation-Id ": "value"})
159+
160+
assert config.headers == {"X-Correlation-Id": "value"}
161+
162+
145163
def test_client_config_accepts_mapping_header_inputs():
146164
headers = MappingProxyType({"X-Correlation-Id": "abc123"})
147165
config = ClientConfig(api_key="test-key", headers=headers)

tests/test_custom_headers.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ def test_sync_transport_rejects_non_string_header_pairs():
2828
SyncTransport(api_key="test-key", headers={"X-Correlation-Id": 123}) # type: ignore[dict-item]
2929

3030

31+
def test_sync_transport_rejects_empty_header_name():
32+
with pytest.raises(HyperbrowserError, match="header names must not be empty"):
33+
SyncTransport(api_key="test-key", headers={" ": "value"})
34+
35+
36+
def test_sync_transport_rejects_header_newline_values():
37+
with pytest.raises(
38+
HyperbrowserError, match="headers must not contain newline characters"
39+
):
40+
SyncTransport(api_key="test-key", headers={"X-Correlation-Id": "bad\nvalue"})
41+
42+
3143
def test_async_transport_accepts_custom_headers():
3244
async def run() -> None:
3345
transport = AsyncTransport(
@@ -49,6 +61,18 @@ def test_async_transport_rejects_non_string_header_pairs():
4961
AsyncTransport(api_key="test-key", headers={"X-Correlation-Id": 123}) # type: ignore[dict-item]
5062

5163

64+
def test_async_transport_rejects_empty_header_name():
65+
with pytest.raises(HyperbrowserError, match="header names must not be empty"):
66+
AsyncTransport(api_key="test-key", headers={" ": "value"})
67+
68+
69+
def test_async_transport_rejects_header_newline_values():
70+
with pytest.raises(
71+
HyperbrowserError, match="headers must not contain newline characters"
72+
):
73+
AsyncTransport(api_key="test-key", headers={"X-Correlation-Id": "bad\nvalue"})
74+
75+
5276
def test_sync_client_config_headers_are_applied_to_transport():
5377
client = Hyperbrowser(
5478
config=ClientConfig(api_key="test-key", headers={"X-Team-Trace": "team-1"})

0 commit comments

Comments
 (0)