diff --git a/.secrets.baseline b/.secrets.baseline index 666c3c96f2..2c0c968d25 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "(?x)( package-lock\\.json$ |Cargo\\.lock$ |uv\\.lock$ |go\\.sum$ |mcpgateway/sri_hashes\\.json$ )|^.secrets.baseline$", "lines": null }, - "generated_at": "2026-05-05T12:37:46Z", + "generated_at": "2026-05-04T09:02:11Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -9706,7 +9706,7 @@ "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 1142, + "line_number": 1151, "type": "Basic Auth Credentials", "verified_result": null } diff --git a/mcpgateway/common/validators.py b/mcpgateway/common/validators.py index 28b0c5d336..a7ef3f79b3 100644 --- a/mcpgateway/common/validators.py +++ b/mcpgateway/common/validators.py @@ -1240,12 +1240,6 @@ def validate_url(cls, value: str, field_name: str = "URL") -> str: if pattern.search(decoded_value): raise ValueError(f"{field_name} contains unsupported or potentially dangerous protocol") - # Block IPv6 URLs (square brackets). Scanning `decoded_value` alone - # suffices: unquote() never removes non-`%` chars, so any `[` in - # `value` also appears in `decoded_value`; `%5B` adds a `[` only there. - if "[" in decoded_value or "]" in decoded_value: - raise ValueError(f"{field_name} contains IPv6 address which is not supported") - # Block protocol-relative URLs if value.startswith("//"): raise ValueError(f"{field_name} contains protocol-relative URL which is not supported") @@ -1275,6 +1269,11 @@ def validate_url(cls, value: str, field_name: str = "URL") -> str: # urlparse does not decode netloc; decode to catch `exam%20ple.com`-style # authority injection without breaking encoded-space in path/query. decoded_netloc = _unquote_if_needed(result.netloc) + + # Check for brackets in decoded netloc (catches URL-encoded IPv6 like %5B::1%5D) + if "[" in decoded_netloc or "]" in decoded_netloc: + raise ValueError(f"{field_name} contains IPv6 address which is not supported") + if any(ch.isspace() for ch in decoded_netloc): raise ValueError(f"{field_name} contains spaces which are not allowed in URLs") diff --git a/tests/unit/mcpgateway/validation/test_validators_advanced.py b/tests/unit/mcpgateway/validation/test_validators_advanced.py index 8d8e3881d7..101068e909 100644 --- a/tests/unit/mcpgateway/validation/test_validators_advanced.py +++ b/tests/unit/mcpgateway/validation/test_validators_advanced.py @@ -1124,6 +1124,15 @@ class TestValidateUrlSecurity: def test_ipv6_blocked(self): with pytest.raises(ValueError, match="IPv6"): SecurityValidator.validate_url("https://[::1]/path") + def test_brackets_in_query_params_allowed(self): + """Brackets in query parameters should be allowed (common API pattern).""" + # URL-encoded brackets in query params (Laravel, Spring, OData, JSON:API style) + assert SecurityValidator.validate_url("https://api.example.com/ingredients?filter%5Bname%5D=value", "URL") + assert SecurityValidator.validate_url("https://api.example.com/items?sort%5B0%5D=name&sort%5B1%5D=created_at", "URL") + assert SecurityValidator.validate_url("https://api.example.com/data?include%5B%5D=relation", "URL") + # Literal brackets in query params (if not URL-encoded by client) + assert SecurityValidator.validate_url("https://api.example.com/items?filter[status]=active", "URL") + def test_crlf_injection(self): with pytest.raises(ValueError, match="control characters"):