diff --git a/strix/interface/cloud/http.py b/strix/interface/cloud/http.py index 6c4da3ff..3f9e5c68 100644 --- a/strix/interface/cloud/http.py +++ b/strix/interface/cloud/http.py @@ -201,7 +201,14 @@ def upload_file(signed_url: str, upload_token: str, path: Path) -> None: def _validate_upload_url(signed_url: str) -> None: """Allow uploads only to the trusted app origin or managed Supabase storage.""" - target = _parse_origin_url(signed_url, label="source upload URL") + # Supabase signed upload URLs carry their signature in the query string. + # Keep every origin/path restriction below, but allow that opaque query on + # this one platform-issued URL type. + target = _parse_origin_url( + signed_url, + label="source upload URL", + allow_query=True, + ) if not target.path.startswith(_STORAGE_PATH_PREFIX): raise CloudError("source upload refused a URL outside the storage API") @@ -226,7 +233,12 @@ def _validate_upload_url(signed_url: str) -> None: ) -def _parse_origin_url(value: str, *, label: str) -> SplitResult: +def _parse_origin_url( + value: str, + *, + label: str, + allow_query: bool = False, +) -> SplitResult: try: parsed = urlsplit(value) port = parsed.port @@ -238,7 +250,7 @@ def _parse_origin_url(value: str, *, label: str) -> SplitResult: or not hostname or parsed.username is not None or parsed.password is not None - or parsed.query + or (parsed.query and not allow_query) or parsed.fragment or "\\" in value or any(character.isspace() for character in value) diff --git a/tests/test_cloud_cli_runtime.py b/tests/test_cloud_cli_runtime.py index 842aaa25..c62365de 100644 --- a/tests/test_cloud_cli_runtime.py +++ b/tests/test_cloud_cli_runtime.py @@ -1026,7 +1026,7 @@ def test_source_upload_rejects_untrusted_destinations_before_reading_file( [ ( "https://app.strix.ai", - "https://project-ref.supabase.co/storage/v1/object/upload/sign/bucket/file", + "https://project-ref.supabase.co/storage/v1/object/upload/sign/bucket/file?token=signed%2Fvalue", ), ( "https://strix.corp.internal",