Skip to content

Commit b393ad9

Browse files
Harden base URL validation for encoded host and credentials
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 47c7f4d commit b393ad9

File tree

3 files changed

+46
-0
lines changed

3 files changed

+46
-0
lines changed

hyperbrowser/config.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ def normalize_base_url(base_url: str) -> str:
5858
raise HyperbrowserError(
5959
"base_url must not include query parameters or fragments"
6060
)
61+
if parsed_base_url.username is not None or parsed_base_url.password is not None:
62+
raise HyperbrowserError("base_url must not include user credentials")
6163

6264
decoded_base_path = parsed_base_url.path
6365
for _ in range(10):
@@ -79,6 +81,24 @@ def normalize_base_url(base_url: str) -> str:
7981
raise HyperbrowserError(
8082
"base_url path must not contain relative path segments"
8183
)
84+
85+
decoded_base_netloc = parsed_base_url.netloc
86+
for _ in range(10):
87+
next_decoded_base_netloc = unquote(decoded_base_netloc)
88+
if next_decoded_base_netloc == decoded_base_netloc:
89+
break
90+
decoded_base_netloc = next_decoded_base_netloc
91+
if "\\" in decoded_base_netloc:
92+
raise HyperbrowserError("base_url host must not contain backslashes")
93+
if any(character.isspace() for character in decoded_base_netloc):
94+
raise HyperbrowserError(
95+
"base_url host must not contain whitespace characters"
96+
)
97+
if any(
98+
ord(character) < 32 or ord(character) == 127
99+
for character in decoded_base_netloc
100+
):
101+
raise HyperbrowserError("base_url host must not contain control characters")
82102
return normalized_base_url
83103

84104
@classmethod

tests/test_config.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ def test_client_config_rejects_empty_or_invalid_base_url():
208208

209209
with pytest.raises(HyperbrowserError, match="must not include query parameters"):
210210
ClientConfig(api_key="test-key", base_url="https://example.local#frag")
211+
with pytest.raises(
212+
HyperbrowserError, match="base_url must not include user credentials"
213+
):
214+
ClientConfig(api_key="test-key", base_url="https://user:pass@example.local")
211215

212216
with pytest.raises(
213217
HyperbrowserError, match="base_url must not contain newline characters"
@@ -360,6 +364,10 @@ def test_client_config_normalize_base_url_validates_and_normalizes():
360364
HyperbrowserError, match="base_url must not contain control characters"
361365
):
362366
ClientConfig.normalize_base_url("https://example.local\x00api")
367+
with pytest.raises(
368+
HyperbrowserError, match="base_url must not include user credentials"
369+
):
370+
ClientConfig.normalize_base_url("https://user:pass@example.local")
363371
with pytest.raises(
364372
HyperbrowserError, match="base_url path must not contain relative path segments"
365373
):
@@ -372,3 +380,15 @@ def test_client_config_normalize_base_url_validates_and_normalizes():
372380
HyperbrowserError, match="base_url must not contain whitespace characters"
373381
):
374382
ClientConfig.normalize_base_url("https://example.local/%2520api")
383+
with pytest.raises(
384+
HyperbrowserError, match="base_url host must not contain backslashes"
385+
):
386+
ClientConfig.normalize_base_url("https://example.local%255C")
387+
with pytest.raises(
388+
HyperbrowserError, match="base_url host must not contain whitespace characters"
389+
):
390+
ClientConfig.normalize_base_url("https://example.local%2520")
391+
with pytest.raises(
392+
HyperbrowserError, match="base_url host must not contain control characters"
393+
):
394+
ClientConfig.normalize_base_url("https://example.local%2500")

tests/test_url_building.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ def test_client_build_url_rejects_runtime_invalid_base_url_changes():
129129
):
130130
client._build_url("/session")
131131

132+
client.config.base_url = "https://user:pass@example.local"
133+
with pytest.raises(
134+
HyperbrowserError, match="base_url must not include user credentials"
135+
):
136+
client._build_url("/session")
137+
132138
client.config.base_url = " "
133139
with pytest.raises(HyperbrowserError, match="base_url must not be empty"):
134140
client._build_url("/session")

0 commit comments

Comments
 (0)