2025-07-14 14:28:11 +02:00
"""
Security utilities for SuperClaude installation system
Path validation and input sanitization
"""
import re
import os
from pathlib import Path
from typing import List , Optional , Tuple , Set
import urllib . parse
class SecurityValidator :
""" Security validation utilities """
# Dangerous path patterns
DANGEROUS_PATTERNS = [
r ' \ . \ ./ ' , # Directory traversal
r ' \ . \ . \ . ' , # Directory traversal
r ' //+ ' , # Multiple slashes
r ' /etc/ ' , # System directories
r ' /bin/ ' ,
r ' /sbin/ ' ,
r ' /usr/bin/ ' ,
r ' /usr/sbin/ ' ,
r ' /var/ ' ,
r ' /tmp/ ' ,
r ' /dev/ ' ,
r ' /proc/ ' ,
r ' /sys/ ' ,
r ' c: \\ windows \\ ' , # Windows system dirs
r ' c: \\ program files \\ ' ,
2025-07-14 23:03:24 +08:00
# Note: Removed c:\\users\\ to allow installation in user directories
# Claude Code installs to user home directory by default
2025-07-14 14:28:11 +02:00
]
# Dangerous filename patterns
DANGEROUS_FILENAMES = [
r ' \ .exe$ ' , # Executables
r ' \ .bat$ ' ,
r ' \ .cmd$ ' ,
r ' \ .scr$ ' ,
r ' \ .dll$ ' ,
r ' \ .so$ ' ,
r ' \ .dylib$ ' ,
r ' passwd ' , # System files
r ' shadow ' ,
r ' hosts ' ,
r ' \ .ssh/ ' ,
r ' \ .aws/ ' ,
r ' \ .env ' , # Environment files
r ' \ .secret ' ,
]
# Allowed file extensions for installation
ALLOWED_EXTENSIONS = {
' .md ' , ' .json ' , ' .py ' , ' .js ' , ' .ts ' , ' .jsx ' , ' .tsx ' ,
' .txt ' , ' .yml ' , ' .yaml ' , ' .toml ' , ' .cfg ' , ' .conf ' ,
' .sh ' , ' .ps1 ' , ' .html ' , ' .css ' , ' .svg ' , ' .png ' , ' .jpg ' , ' .gif '
}
# Maximum path lengths
MAX_PATH_LENGTH = 4096
MAX_FILENAME_LENGTH = 255
@classmethod
def validate_path ( cls , path : Path , base_dir : Optional [ Path ] = None ) - > Tuple [ bool , str ] :
"""
Validate path for security issues
Args :
path : Path to validate
base_dir : Base directory that path should be within
Returns :
Tuple of ( is_safe : bool , error_message : str )
"""
try :
# Convert to absolute path
abs_path = path . resolve ( )
path_str = str ( abs_path ) . lower ( )
# Check path length
if len ( str ( abs_path ) ) > cls . MAX_PATH_LENGTH :
return False , f " Path too long: { len ( str ( abs_path ) ) } > { cls . MAX_PATH_LENGTH } "
# Check filename length
if len ( abs_path . name ) > cls . MAX_FILENAME_LENGTH :
return False , f " Filename too long: { len ( abs_path . name ) } > { cls . MAX_FILENAME_LENGTH } "
# Check for dangerous patterns
for pattern in cls . DANGEROUS_PATTERNS :
if re . search ( pattern , path_str , re . IGNORECASE ) :
return False , f " Dangerous path pattern detected: { pattern } "
# Check for dangerous filenames
for pattern in cls . DANGEROUS_FILENAMES :
if re . search ( pattern , abs_path . name , re . IGNORECASE ) :
return False , f " Dangerous filename pattern detected: { pattern } "
# Check if path is within base directory
if base_dir :
base_abs = base_dir . resolve ( )
try :
abs_path . relative_to ( base_abs )
except ValueError :
return False , f " Path outside allowed directory: { abs_path } not in { base_abs } "
# Check for null bytes
if ' \x00 ' in str ( path ) :
return False , " Null byte detected in path "
# Check for Windows reserved names
if os . name == ' nt ' :
reserved_names = [
' CON ' , ' PRN ' , ' AUX ' , ' NUL ' ,
' COM1 ' , ' COM2 ' , ' COM3 ' , ' COM4 ' , ' COM5 ' , ' COM6 ' , ' COM7 ' , ' COM8 ' , ' COM9 ' ,
' LPT1 ' , ' LPT2 ' , ' LPT3 ' , ' LPT4 ' , ' LPT5 ' , ' LPT6 ' , ' LPT7 ' , ' LPT8 ' , ' LPT9 '
]
name_without_ext = abs_path . stem . upper ( )
if name_without_ext in reserved_names :
return False , f " Reserved Windows filename: { name_without_ext } "
return True , " Path is safe "
except Exception as e :
return False , f " Path validation error: { e } "
@classmethod
def validate_file_extension ( cls , path : Path ) - > Tuple [ bool , str ] :
"""
Validate file extension is allowed
Args :
path : Path to validate
Returns :
Tuple of ( is_allowed : bool , message : str )
"""
extension = path . suffix . lower ( )
if not extension :
return True , " No extension (allowed) "
if extension in cls . ALLOWED_EXTENSIONS :
return True , f " Extension { extension } is allowed "
else :
return False , f " Extension { extension } is not allowed "
@classmethod
def sanitize_filename ( cls , filename : str ) - > str :
"""
Sanitize filename by removing dangerous characters
Args :
filename : Original filename
Returns :
Sanitized filename
"""
# Remove null bytes
filename = filename . replace ( ' \x00 ' , ' ' )
# Remove or replace dangerous characters
dangerous_chars = r ' [<>: " / \\ |?* \ x00- \ x1f] '
filename = re . sub ( dangerous_chars , ' _ ' , filename )
# Remove leading/trailing dots and spaces
filename = filename . strip ( ' . ' )
# Ensure not empty
if not filename :
filename = ' unnamed '
# Truncate if too long
if len ( filename ) > cls . MAX_FILENAME_LENGTH :
name , ext = os . path . splitext ( filename )
max_name_len = cls . MAX_FILENAME_LENGTH - len ( ext )
filename = name [ : max_name_len ] + ext
# Check for Windows reserved names
if os . name == ' nt ' :
name_without_ext = os . path . splitext ( filename ) [ 0 ] . upper ( )
reserved_names = [
' CON ' , ' PRN ' , ' AUX ' , ' NUL ' ,
' COM1 ' , ' COM2 ' , ' COM3 ' , ' COM4 ' , ' COM5 ' , ' COM6 ' , ' COM7 ' , ' COM8 ' , ' COM9 ' ,
' LPT1 ' , ' LPT2 ' , ' LPT3 ' , ' LPT4 ' , ' LPT5 ' , ' LPT6 ' , ' LPT7 ' , ' LPT8 ' , ' LPT9 '
]
if name_without_ext in reserved_names :
filename = f " safe_ { filename } "
return filename
@classmethod
def sanitize_input ( cls , user_input : str , max_length : int = 1000 ) - > str :
"""
Sanitize user input
Args :
user_input : Raw user input
max_length : Maximum allowed length
Returns :
Sanitized input
"""
if not user_input :
return " "
# Remove null bytes and control characters
sanitized = re . sub ( r ' [ \ x00- \ x08 \ x0b \ x0c \ x0e- \ x1f \ x7f] ' , ' ' , user_input )
# Trim whitespace
sanitized = sanitized . strip ( )
# Truncate if too long
if len ( sanitized ) > max_length :
sanitized = sanitized [ : max_length ]
return sanitized
@classmethod
def validate_url ( cls , url : str ) - > Tuple [ bool , str ] :
"""
Validate URL for security issues
Args :
url : URL to validate
Returns :
Tuple of ( is_safe : bool , message : str )
"""
try :
parsed = urllib . parse . urlparse ( url )
# Check scheme
if parsed . scheme not in [ ' http ' , ' https ' ] :
return False , f " Invalid scheme: { parsed . scheme } "
# Check for localhost/private IPs (basic check)
hostname = parsed . hostname
if hostname :
if hostname . lower ( ) in [ ' localhost ' , ' 127.0.0.1 ' , ' ::1 ' ] :
return False , " Localhost URLs not allowed "
# Basic private IP check
if hostname . startswith ( ' 192.168. ' ) or hostname . startswith ( ' 10. ' ) or hostname . startswith ( ' 172. ' ) :
return False , " Private IP addresses not allowed "
# Check URL length
if len ( url ) > 2048 :
return False , " URL too long "
return True , " URL is safe "
except Exception as e :
return False , f " URL validation error: { e } "
@classmethod
def check_permissions ( cls , path : Path , required_permissions : Set [ str ] ) - > Tuple [ bool , List [ str ] ] :
"""
Check file / directory permissions
Args :
path : Path to check
required_permissions : Set of required permissions ( ' read ' , ' write ' , ' execute ' )
Returns :
Tuple of ( has_permissions : bool , missing_permissions : List [ str ] )
"""
missing = [ ]
try :
if not path . exists ( ) :
# For non-existent paths, check parent directory
parent = path . parent
if not parent . exists ( ) :
missing . append ( " path does not exist " )
return False , missing
path = parent
if ' read ' in required_permissions :
if not os . access ( path , os . R_OK ) :
missing . append ( ' read ' )
if ' write ' in required_permissions :
if not os . access ( path , os . W_OK ) :
missing . append ( ' write ' )
if ' execute ' in required_permissions :
if not os . access ( path , os . X_OK ) :
missing . append ( ' execute ' )
return len ( missing ) == 0 , missing
except Exception as e :
missing . append ( f " permission check error: { e } " )
return False , missing
@classmethod
def validate_installation_target ( cls , target_dir : Path ) - > Tuple [ bool , List [ str ] ] :
"""
2025-07-14 17:18:11 +02:00
Validate installation target directory with enhanced Windows compatibility
2025-07-14 14:28:11 +02:00
Args :
target_dir : Target installation directory
Returns :
Tuple of ( is_safe : bool , error_messages : List [ str ] )
"""
errors = [ ]
2025-07-14 17:18:11 +02:00
# Enhanced path resolution with Windows normalization
try :
abs_target = target_dir . resolve ( )
except Exception as e :
errors . append ( f " Cannot resolve target path: { e } " )
return False , errors
# Windows-specific path normalization
if os . name == ' nt ' :
# Normalize Windows paths for consistent comparison
abs_target_str = str ( abs_target ) . lower ( ) . replace ( ' / ' , ' \\ ' )
else :
abs_target_str = str ( abs_target ) . lower ( )
2025-07-14 23:03:24 +08:00
# Special handling for Claude installation directory
2025-07-14 17:18:11 +02:00
claude_patterns = [ ' .claude ' , ' .claude ' + os . sep , ' .claude \\ ' , ' .claude/ ' ]
is_claude_dir = any ( abs_target_str . endswith ( pattern ) for pattern in claude_patterns )
2025-07-14 23:03:24 +08:00
2025-07-14 17:18:11 +02:00
if is_claude_dir :
2025-07-14 23:03:24 +08:00
try :
2025-07-14 17:18:11 +02:00
home_path = Path . home ( )
except ( RuntimeError , OSError ) :
# If we can't determine home directory, skip .claude special handling
cls . _log_security_decision ( " WARN " , f " Cannot determine home directory for .claude validation: { abs_target } " )
# Fall through to regular validation
else :
try :
# Verify it's specifically the current user's home directory
abs_target . relative_to ( home_path )
# Enhanced Windows security checks for .claude directories
if os . name == ' nt ' :
# Check for junction points and symbolic links on Windows
if cls . _is_windows_junction_or_symlink ( abs_target ) :
errors . append ( " Installation to junction points or symbolic links is not allowed for security " )
return False , errors
# Additional validation: verify it's in a user profile directory structure
# Only check if it looks like a Windows path (contains drive letter)
if ' : ' in abs_target_str and ' \\ users \\ ' in abs_target_str :
current_user = os . environ . get ( ' USERNAME ' , ' ' )
if current_user and f ' \\ users \\ { current_user . lower ( ) } \\ ' not in abs_target_str :
errors . append ( f " Installation must be in current user ' s directory ( { current_user } ) " )
return False , errors
# Check permissions
has_perms , missing = cls . check_permissions ( target_dir , { ' read ' , ' write ' } )
if not has_perms :
if os . name == ' nt ' :
errors . append ( f " Insufficient permissions for Windows installation: { missing } . Try running as administrator or check folder permissions. " )
else :
errors . append ( f " Insufficient permissions: missing { missing } " )
# Log successful validation for audit trail
cls . _log_security_decision ( " ALLOW " , f " Claude directory installation validated: { abs_target } " )
return len ( errors ) == 0 , errors
except ValueError :
# Not under current user's home directory
if os . name == ' nt ' :
errors . append ( " Claude installation must be in your user directory (e.g., C: \\ Users \\ YourName \\ .claude) " )
else :
errors . append ( " Claude installation must be in your home directory (e.g., ~/.claude) " )
cls . _log_security_decision ( " DENY " , f " Claude directory outside user home: { abs_target } " )
return False , errors
2025-07-14 23:03:24 +08:00
# Validate path for non-.claude directories
2025-07-14 14:28:11 +02:00
is_safe , msg = cls . validate_path ( target_dir )
if not is_safe :
2025-07-14 17:18:11 +02:00
if os . name == ' nt ' :
# Enhanced Windows error messages
if " dangerous path pattern " in msg . lower ( ) :
errors . append ( f " Invalid Windows path: { msg } . Ensure path doesn ' t contain dangerous patterns or reserved directories. " )
elif " path too long " in msg . lower ( ) :
errors . append ( f " Windows path too long: { msg } . Windows has a 260 character limit for most paths. " )
elif " reserved " in msg . lower ( ) :
errors . append ( f " Windows reserved name: { msg } . Avoid names like CON, PRN, AUX, NUL, COM1-9, LPT1-9. " )
else :
errors . append ( f " Invalid target path: { msg } " )
else :
errors . append ( f " Invalid target path: { msg } " )
# Check permissions with platform-specific guidance
2025-07-14 14:28:11 +02:00
has_perms , missing = cls . check_permissions ( target_dir , { ' read ' , ' write ' } )
if not has_perms :
2025-07-14 17:18:11 +02:00
if os . name == ' nt ' :
errors . append ( f " Insufficient Windows permissions: { missing } . Try running as administrator or check folder security settings in Properties > Security. " )
else :
errors . append ( f " Insufficient permissions: { missing } . Try: chmod 755 { target_dir } " )
2025-07-14 14:28:11 +02:00
2025-07-14 17:18:11 +02:00
# Check if it's a system directory with enhanced messages
2025-07-14 14:28:11 +02:00
system_dirs = [
Path ( ' /etc ' ) , Path ( ' /bin ' ) , Path ( ' /sbin ' ) , Path ( ' /usr/bin ' ) , Path ( ' /usr/sbin ' ) ,
Path ( ' /var ' ) , Path ( ' /tmp ' ) , Path ( ' /dev ' ) , Path ( ' /proc ' ) , Path ( ' /sys ' )
]
if os . name == ' nt ' :
system_dirs . extend ( [
Path ( ' C: \\ Windows ' ) , Path ( ' C: \\ Program Files ' ) , Path ( ' C: \\ Program Files (x86) ' )
] )
for sys_dir in system_dirs :
try :
if abs_target . is_relative_to ( sys_dir ) :
2025-07-14 17:18:11 +02:00
if os . name == ' nt ' :
errors . append ( f " Cannot install to Windows system directory: { sys_dir } . Use a location in your user profile instead (e.g., C: \\ Users \\ YourName \\ ). " )
else :
errors . append ( f " Cannot install to system directory: { sys_dir } . Use a location in your home directory instead (~/). " )
cls . _log_security_decision ( " DENY " , f " Attempted installation to system directory: { sys_dir } " )
2025-07-14 14:28:11 +02:00
break
except ( ValueError , AttributeError ) :
# is_relative_to not available in older Python versions
try :
abs_target . relative_to ( sys_dir )
errors . append ( f " Cannot install to system directory: { sys_dir } " )
break
except ValueError :
continue
return len ( errors ) == 0 , errors
@classmethod
def validate_component_files ( cls , file_list : List [ Tuple [ Path , Path ] ] , base_source_dir : Path , base_target_dir : Path ) - > Tuple [ bool , List [ str ] ] :
"""
Validate list of files for component installation
Args :
file_list : List of ( source , target ) path tuples
base_source_dir : Base source directory
base_target_dir : Base target directory
Returns :
Tuple of ( all_safe : bool , error_messages : List [ str ] )
"""
errors = [ ]
for source , target in file_list :
# Validate source path
is_safe , msg = cls . validate_path ( source , base_source_dir )
if not is_safe :
errors . append ( f " Invalid source path { source } : { msg } " )
# Validate target path
is_safe , msg = cls . validate_path ( target , base_target_dir )
if not is_safe :
errors . append ( f " Invalid target path { target } : { msg } " )
# Validate file extension
is_allowed , msg = cls . validate_file_extension ( source )
if not is_allowed :
errors . append ( f " File { source } : { msg } " )
return len ( errors ) == 0 , errors
2025-07-14 17:18:11 +02:00
@classmethod
def _is_windows_junction_or_symlink ( cls , path : Path ) - > bool :
"""
Check if path is a Windows junction point or symbolic link
Args :
path : Path to check
Returns :
True if path is a junction point or symlink , False otherwise
"""
if os . name != ' nt ' :
return False
try :
# Only check if path exists to avoid filesystem errors during testing
if not path . exists ( ) :
return False
# Check if path is a symlink (covers most cases)
if path . is_symlink ( ) :
return True
# Additional Windows-specific checks for junction points
try :
import stat
st = path . stat ( )
# Check for reparse point (junction points have this attribute)
if hasattr ( st , ' st_reparse_tag ' ) and st . st_reparse_tag != 0 :
return True
except ( OSError , AttributeError ) :
pass
# Alternative method using os.path.islink
try :
if os . path . islink ( str ( path ) ) :
return True
except ( OSError , AttributeError ) :
pass
except ( OSError , AttributeError , NotImplementedError ) :
# If we can't determine safely, default to False
# This ensures the function doesn't break validation
pass
return False
@classmethod
def _log_security_decision ( cls , action : str , message : str ) - > None :
"""
Log security validation decisions for audit trail
Args :
action : Security action taken ( ALLOW , DENY , WARN )
message : Description of the decision
"""
try :
import logging
import datetime
# Create security logger if it doesn't exist
security_logger = logging . getLogger ( ' superclaude.security ' )
if not security_logger . handlers :
# Set up basic logging if not already configured
handler = logging . StreamHandler ( )
formatter = logging . Formatter (
' %(asctime)s - SECURITY - %(levelname)s - %(message)s '
)
handler . setFormatter ( formatter )
security_logger . addHandler ( handler )
security_logger . setLevel ( logging . INFO )
# Log the security decision
timestamp = datetime . datetime . now ( ) . isoformat ( )
log_message = f " [ { action } ] { message } (PID: { os . getpid ( ) } ) "
if action == " DENY " :
security_logger . warning ( log_message )
else :
security_logger . info ( log_message )
except Exception :
# Don't fail security validation if logging fails
pass
2025-07-14 14:28:11 +02:00
@classmethod
def create_secure_temp_dir ( cls , prefix : str = " superclaude_ " ) - > Path :
"""
Create secure temporary directory
Args :
prefix : Prefix for temp directory name
Returns :
Path to secure temporary directory
"""
import tempfile
# Create with secure permissions (0o700)
temp_dir = Path ( tempfile . mkdtemp ( prefix = prefix ) )
temp_dir . chmod ( 0o700 )
return temp_dir
@classmethod
def secure_delete ( cls , path : Path ) - > bool :
"""
Securely delete file or directory
Args :
path : Path to delete
Returns :
True if successful , False otherwise
"""
try :
if not path . exists ( ) :
return True
if path . is_file ( ) :
# Overwrite file with random data before deletion
try :
import secrets
file_size = path . stat ( ) . st_size
with open ( path , ' r+b ' ) as f :
# Overwrite with random data
f . write ( secrets . token_bytes ( file_size ) )
f . flush ( )
os . fsync ( f . fileno ( ) )
except Exception :
pass # If overwrite fails, still try to delete
path . unlink ( )
elif path . is_dir ( ) :
# Recursively delete directory contents
import shutil
shutil . rmtree ( path )
return True
except Exception :
return False