# Copyright (c) 2023-present Plane Software, Inc. and contributors # SPDX-License-Identifier: AGPL-3.0-only # See the LICENSE file for details. # Django imports from django.utils.http import url_has_allowed_host_and_scheme from django.conf import settings # Python imports import os from urllib.parse import urlparse def sanitize_filename(filename): """ Sanitize a filename to prevent path traversal attacks. Strips directory components, path traversal sequences, and control characters from user-supplied filenames used in upload paths and S3 object keys. Returns None for empty/missing input so callers can still validate that a filename was provided. """ if not filename or not isinstance(filename, str): return None # Strip ASCII control characters (0-31 and 127), including null bytes filename = "".join(char for char in filename if not (ord(char) < 32 or ord(char) == 127)) # Normalize backslashes so os.path.basename handles Windows-style paths on POSIX filename = filename.replace("\\", "/") # Take only the basename to remove any directory components filename = os.path.basename(filename) # Remove any remaining path traversal sequences filename = filename.replace("..", "") # Strip whitespace before removing leading dots so " .env" is caught filename = filename.strip() # Remove leading dots (hidden files) filename = filename.lstrip(".") # Strip any remaining whitespace filename = filename.strip() if not filename: return None return filename def _contains_suspicious_patterns(path: str) -> bool: """ Check for suspicious patterns that might indicate malicious intent. Args: path (str): The path to check Returns: bool: True if suspicious patterns found, False otherwise """ suspicious_patterns = [ r"javascript:", # JavaScript injection r"data:", # Data URLs r"vbscript:", # VBScript injection r"file:", # File protocol r"ftp:", # FTP protocol r"%2e%2e", # URL encoded path traversal r"%2f%2f", # URL encoded double slash r"%5c%5c", # URL encoded backslashes r" str: """Validates that next_path is a safe relative path for redirection.""" # Browsers interpret backslashes as forward slashes. Remove all backslashes. if not next_path or not isinstance(next_path, str): return "" # Limit input length to prevent DoS attacks if len(next_path) > 500: return "" next_path = next_path.replace("\\", "") parsed_url = urlparse(next_path) # Block absolute URLs or anything with scheme/netloc if parsed_url.scheme or parsed_url.netloc: next_path = parsed_url.path # Extract only the path component # Must start with a forward slash and not be empty if not next_path or not next_path.startswith("/"): return "" # Prevent path traversal if ".." in next_path: return "" # Additional security checks if _contains_suspicious_patterns(next_path): return "" return next_path def get_safe_redirect_url(base_url: str, next_path: str = "", params: dict = {}) -> str: """ Safely construct a redirect URL with validated next_path. Args: base_url (str): The base URL to redirect to next_path (str): The next path to append params (dict): The parameters to append Returns: str: The safe redirect URL """ from urllib.parse import urlencode # Validate the next path validated_path = validate_next_path(next_path) # Add the next path to the parameters base_url = base_url.rstrip("/") # Prepare the query parameters query_parts = [] encoded_params = "" # Add the next path to the parameters if validated_path: query_parts.append(f"next_path={validated_path}") # Add additional parameters if params: encoded_params = urlencode(params) query_parts.append(encoded_params) # Construct the url query string if query_parts: query_string = "&".join(query_parts) url = f"{base_url}/?{query_string}" else: url = base_url # 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 + (f"?{encoded_params}" if encoded_params else "")