Skip to content
Merged
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
6 changes: 5 additions & 1 deletion apps/api/plane/authentication/views/space/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.core.validators import validate_email
from django.http import HttpResponseRedirect
from django.views import View
from django.utils.http import url_has_allowed_host_and_scheme
Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

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

Missing import for get_allowed_hosts function. The function is used on line 204 but not imported, which will cause a NameError at runtime.

Copilot uses AI. Check for mistakes.

# Module imports
from plane.authentication.provider.credentials.email import EmailProvider
Expand Down Expand Up @@ -200,7 +201,10 @@ def post(self, request):
# redirect to referer path
next_path = validate_next_path(next_path=next_path)
url = f"{base_host(request=request, is_space=True).rstrip("/")}{next_path}"
return HttpResponseRedirect(url)
if url_has_allowed_host_and_scheme(url, allowed_hosts=get_allowed_hosts()):
return HttpResponseRedirect(url)
else:
return HttpResponseRedirect(base_host(request=request, is_space=True))
except AuthenticationException as e:
params = e.get_error_dict()
url = get_safe_redirect_url(
Expand Down
13 changes: 10 additions & 3 deletions apps/api/plane/authentication/views/space/magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.core.validators import validate_email
from django.http import HttpResponseRedirect
from django.views import View
from django.utils.http import url_has_allowed_host_and_scheme

# Third party imports
from rest_framework import status
Expand All @@ -20,7 +21,7 @@
AuthenticationException,
AUTHENTICATION_ERROR_CODES,
)
from plane.utils.path_validator import get_safe_redirect_url, validate_next_path
from plane.utils.path_validator import get_safe_redirect_url, validate_next_path, get_allowed_hosts


class MagicGenerateSpaceEndpoint(APIView):
Expand Down Expand Up @@ -96,7 +97,10 @@ def post(self, request):
# redirect to referer path
next_path = validate_next_path(next_path=next_path)
url = f"{base_host(request=request, is_space=True).rstrip("/")}{next_path}"
return HttpResponseRedirect(url)
if url_has_allowed_host_and_scheme(url, allowed_hosts=get_allowed_hosts()):
return HttpResponseRedirect(url)
else:
return HttpResponseRedirect(base_host(request=request, is_space=True))

except AuthenticationException as e:
params = e.get_error_dict()
Expand Down Expand Up @@ -155,7 +159,10 @@ def post(self, request):
# redirect to referer path
next_path = validate_next_path(next_path=next_path)
url = f"{base_host(request=request, is_space=True).rstrip("/")}{next_path}"
return HttpResponseRedirect(url)
if url_has_allowed_host_and_scheme(url, allowed_hosts=get_allowed_hosts()):
return HttpResponseRedirect(url)
else:
return HttpResponseRedirect(base_host(request=request, is_space=True))

except AuthenticationException as e:
params = e.get_error_dict()
Expand Down
31 changes: 29 additions & 2 deletions apps/api/plane/utils/path_validator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Django imports
from django.utils.http import url_has_allowed_host_and_scheme
from django.conf import settings

# Python imports
from urllib.parse import urlparse


def _contains_suspicious_patterns(path: str) -> bool:
"""
Check for suspicious patterns that might indicate malicious intent.
Expand Down Expand Up @@ -38,6 +43,21 @@ def _contains_suspicious_patterns(path: str) -> bool:
return False


def get_allowed_hosts() -> list[str]:
"""Get the allowed hosts from the settings."""
base_origin = settings.WEB_URL or settings.APP_BASE_URL
allowed_hosts = [base_origin]
if settings.ADMIN_BASE_URL:
# Get only the host
host = urlparse(settings.ADMIN_BASE_URL).netloc
allowed_hosts.append(host)
if settings.SPACE_BASE_URL:
# Get only the host
host = urlparse(settings.SPACE_BASE_URL).netloc
allowed_hosts.append(host)
Comment on lines +48 to +57
Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

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

Inconsistent host extraction logic. base_origin is added as a full URL while ADMIN_BASE_URL and SPACE_BASE_URL are parsed to extract only the netloc. This creates a mismatch where base_origin should also be parsed to extract only the host portion for consistent validation.

Copilot uses AI. Check for mistakes.
return allowed_hosts
Copy link

Choose a reason for hiding this comment

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

Bug: Inconsistent Host Handling in URL Validation

The get_allowed_hosts function inconsistently populates the allowed_hosts list. It adds base_origin as a full URL, but extracts only the hostname for ADMIN_BASE_URL and SPACE_BASE_URL. This causes url_has_allowed_host_and_scheme to fail validation, as it expects only hostnames.

Fix in Cursor Fix in Web



def validate_next_path(next_path: str) -> str:
"""Validates that next_path is a safe relative path for redirection."""
# Browsers interpret backslashes as forward slashes. Remove all backslashes.
Expand Down Expand Up @@ -92,7 +112,14 @@ def get_safe_redirect_url(base_url: str, next_path: str = "", params: dict = {})
base_url = base_url.rstrip('/')
if params:
encoded_params = urlencode(params)
return f"{base_url}/?next_path={validated_path}&{encoded_params}"
url = f"{base_url}/?next_path={validated_path}&{encoded_params}"
else:
url = f"{base_url}/?next_path={validated_path}"

return f"{base_url}/?next_path={validated_path}"
# Check if the URL is allowed
if url_has_allowed_host_and_scheme(url, allowed_hosts=get_allowed_hosts()):
return url

# Return the base URL if the URL is not allowed
return base_url

Loading