Skip to content

Commit a91e8e3

Browse files
Validate decoded base URL paths for unsafe segments
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 8d0d0ac commit a91e8e3

File tree

3 files changed

+45
-1
lines changed

3 files changed

+45
-1
lines changed

hyperbrowser/config.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass
2-
from urllib.parse import urlparse
2+
from urllib.parse import unquote, urlparse
33
from typing import Dict, Mapping, Optional
44
import os
55

@@ -58,6 +58,27 @@ def normalize_base_url(base_url: str) -> str:
5858
raise HyperbrowserError(
5959
"base_url must not include query parameters or fragments"
6060
)
61+
62+
decoded_base_path = parsed_base_url.path
63+
for _ in range(10):
64+
next_decoded_base_path = unquote(decoded_base_path)
65+
if next_decoded_base_path == decoded_base_path:
66+
break
67+
decoded_base_path = next_decoded_base_path
68+
if "\\" in decoded_base_path:
69+
raise HyperbrowserError("base_url must not contain backslashes")
70+
if any(character.isspace() for character in decoded_base_path):
71+
raise HyperbrowserError("base_url must not contain whitespace characters")
72+
if any(
73+
ord(character) < 32 or ord(character) == 127
74+
for character in decoded_base_path
75+
):
76+
raise HyperbrowserError("base_url must not contain control characters")
77+
path_segments = [segment for segment in decoded_base_path.split("/") if segment]
78+
if any(segment in {".", ".."} for segment in path_segments):
79+
raise HyperbrowserError(
80+
"base_url path must not contain relative path segments"
81+
)
6182
return normalized_base_url
6283

6384
@classmethod

tests/test_config.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,10 @@ def test_client_config_rejects_empty_or_invalid_base_url():
226226
HyperbrowserError, match="base_url must not contain control characters"
227227
):
228228
ClientConfig(api_key="test-key", base_url="https://example.local\x00api")
229+
with pytest.raises(
230+
HyperbrowserError, match="base_url path must not contain relative path segments"
231+
):
232+
ClientConfig(api_key="test-key", base_url="https://example.local/%2e%2e/api")
229233

230234

231235
def test_client_config_normalizes_headers_to_internal_copy():
@@ -356,3 +360,15 @@ def test_client_config_normalize_base_url_validates_and_normalizes():
356360
HyperbrowserError, match="base_url must not contain control characters"
357361
):
358362
ClientConfig.normalize_base_url("https://example.local\x00api")
363+
with pytest.raises(
364+
HyperbrowserError, match="base_url path must not contain relative path segments"
365+
):
366+
ClientConfig.normalize_base_url("https://example.local/%252e%252e/api")
367+
with pytest.raises(
368+
HyperbrowserError, match="base_url must not contain backslashes"
369+
):
370+
ClientConfig.normalize_base_url("https://example.local/%255Capi")
371+
with pytest.raises(
372+
HyperbrowserError, match="base_url must not contain whitespace characters"
373+
):
374+
ClientConfig.normalize_base_url("https://example.local/%2520api")

tests/test_url_building.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ def test_client_build_url_rejects_runtime_invalid_base_url_changes():
122122
):
123123
client._build_url("/session")
124124

125+
client.config.base_url = "https://example.local/%2e%2e/api"
126+
with pytest.raises(
127+
HyperbrowserError,
128+
match="base_url path must not contain relative path segments",
129+
):
130+
client._build_url("/session")
131+
125132
client.config.base_url = " "
126133
with pytest.raises(HyperbrowserError, match="base_url must not be empty"):
127134
client._build_url("/session")

0 commit comments

Comments
 (0)