Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/sentry/api/endpoints/api_application_details.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from urllib.parse import urlparse

from django.db import router, transaction
from rest_framework import serializers
from rest_framework.authentication import SessionAuthentication
Expand All @@ -14,9 +16,37 @@
from sentry.models.apiapplication import ApiApplication, ApiApplicationStatus


class CustomSchemeURLField(serializers.CharField):
"""URLField that accepts any valid scheme, not just HTTP/HTTPS."""

default_error_messages = {
"invalid": "Enter a valid URL.",
}

def run_validation(self, data):
# First run the standard CharField validations
data = super().run_validation(data)

if data is None:
return data

if data == "":
self.fail("invalid")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: URL Field Ignores Blank Allowance

The CustomSchemeURLField unconditionally rejects empty strings, even when allow_blank=True is configured. This bypasses the intended allow_blank behavior, causing validation to fail unexpectedly for empty inputs.

Fix in Cursor Fix in Web


# Basic URL structure validation
try:
parsed = urlparse(data)
if not parsed.scheme or not parsed.netloc:
self.fail("invalid")
except Exception:
self.fail("invalid")

return data


class ApiApplicationSerializer(serializers.Serializer):
name = serializers.CharField(max_length=64)
redirectUris = ListField(child=serializers.URLField(max_length=255), required=False)
redirectUris = ListField(child=CustomSchemeURLField(max_length=255), required=False)
allowedOrigins = ListField(
# TODO(dcramer): make this validate origins
child=serializers.CharField(max_length=255),
Expand Down
45 changes: 45 additions & 0 deletions tests/sentry/api/endpoints/test_api_application_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,51 @@ def test_simple(self) -> None:
app = ApiApplication.objects.get(id=app.id)
assert app.name == "foobaz"

def test_redirect_uris_with_custom_schemes(self) -> None:
app = ApiApplication.objects.create(owner=self.user, name="a")

self.login_as(self.user)
url = reverse("sentry-api-0-api-application-details", args=[app.client_id])

# Test various custom schemes
custom_uris = [
"myapp://callback",
"custom-scheme://auth/callback",
"app123://redirect",
"http://example.com/callback", # Standard HTTP still works
"https://example.com/callback", # Standard HTTPS still works
]

response = self.client.put(url, data={"redirectUris": custom_uris})
assert response.status_code == 200, (response.status_code, response.content)

app = ApiApplication.objects.get(id=app.id)
saved_uris = app.get_redirect_uris()
assert len(saved_uris) == 5
assert "myapp://callback" in saved_uris
assert "custom-scheme://auth/callback" in saved_uris
assert "app123://redirect" in saved_uris
assert "http://example.com/callback" in saved_uris
assert "https://example.com/callback" in saved_uris

def test_invalid_redirect_uris_rejected(self) -> None:
app = ApiApplication.objects.create(owner=self.user, name="a")

self.login_as(self.user)
url = reverse("sentry-api-0-api-application-details", args=[app.client_id])

# Test invalid URIs
invalid_uris = [
"not-a-url", # No scheme or netloc
"://missing-scheme.com", # No scheme
"scheme-only://", # No netloc
"", # Empty string should be rejected
]

response = self.client.put(url, data={"redirectUris": invalid_uris})
assert response.status_code == 400, (response.status_code, response.content)
assert "redirectUris" in response.data


@control_silo_test
class ApiApplicationDeleteTest(APITestCase):
Expand Down
Loading