From 6d3d9e6df7b070a0281cdf56ef78d7059d56921a Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 16 Sep 2025 21:37:08 +0530 Subject: [PATCH] [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. --- .../plane/authentication/views/space/email.py | 6 +++- .../plane/authentication/views/space/magic.py | 13 ++++++-- apps/api/plane/utils/path_validator.py | 31 +++++++++++++++++-- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/apps/api/plane/authentication/views/space/email.py b/apps/api/plane/authentication/views/space/email.py index d247f6e98..2fb1c2c5e 100644 --- a/apps/api/plane/authentication/views/space/email.py +++ b/apps/api/plane/authentication/views/space/email.py @@ -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( diff --git a/apps/api/plane/authentication/views/space/magic.py b/apps/api/plane/authentication/views/space/magic.py index f50274a4a..85e3a185c 100644 --- a/apps/api/plane/authentication/views/space/magic.py +++ b/apps/api/plane/authentication/views/space/magic.py @@ -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() diff --git a/apps/api/plane/utils/path_validator.py b/apps/api/plane/utils/path_validator.py index e5bf7aeb2..a89c8b969 100644 --- a/apps/api/plane/utils/path_validator.py +++ b/apps/api/plane/utils/path_validator.py @@ -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 \ No newline at end of file