[WEB-4943]: add url has allowed host or scheme for validating valid redirections (#7809)

* feat: enhance path validation and URL safety in path_validator.py

* Added get_allowed_hosts function to retrieve allowed hosts from settings.
* Updated get_safe_redirect_url to validate URLs against allowed hosts.
* Improved URL construction logic for safer redirection handling.

* feat: enhance URL validation in authentication views

* Added url_has_allowed_host_and_scheme checks in SignUpAuthSpaceEndpoint and MagicSignInSpaceEndpoint for safer redirection.
* Updated redirect logic to fallback to base host if the constructed URL is not allowed.
* Improved overall URL safety and handling in authentication flows.

* fix: improve host extraction in get_allowed_hosts function

* Updated get_allowed_hosts to extract only the host from ADMIN_BASE_URL and SPACE_BASE_URL settings for better URL validation.
* Enhanced overall safety and clarity in allowed hosts retrieval.
This commit is contained in:
Nikhil 2025-09-16 21:37:08 +05:30 committed by GitHub
parent d521eab22f
commit 6d3d9e6df7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 44 additions and 6 deletions

View File

@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError
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
# Module imports
from plane.authentication.provider.credentials.email import EmailProvider
@ -200,7 +201,10 @@ class SignUpAuthSpaceEndpoint(View):
# 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(

View File

@ -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
@ -20,7 +21,7 @@ from plane.authentication.adapter.error import (
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):
@ -96,7 +97,10 @@ class MagicSignInSpaceEndpoint(View):
# 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()
@ -155,7 +159,10 @@ class MagicSignUpSpaceEndpoint(View):
# 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()

View File

@ -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.
@ -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)
return allowed_hosts
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.
@ -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