mirror of
https://github.com/whyour/qinglong.git
synced 2025-12-13 07:25:05 +08:00
409 lines
16 KiB
Python
409 lines
16 KiB
Python
import os
|
|
import sys
|
|
import builtins
|
|
from pathlib import Path
|
|
|
|
# Get the QL_DIR and data directory paths
|
|
ql_dir = os.environ.get('QL_DIR', os.path.join(os.path.dirname(__file__), '../..'))
|
|
data_dir = os.environ.get('QL_DATA_DIR', os.path.join(ql_dir, 'data'))
|
|
|
|
# Normalize paths to avoid bypassing with relative paths or symlinks
|
|
try:
|
|
normalized_ql_dir = os.path.realpath(ql_dir)
|
|
normalized_data_dir = os.path.realpath(data_dir)
|
|
except:
|
|
normalized_ql_dir = os.path.abspath(ql_dir)
|
|
normalized_data_dir = os.path.abspath(data_dir)
|
|
|
|
# Protected directories - no write access allowed
|
|
protected_paths = [
|
|
os.path.join(normalized_ql_dir, 'back'),
|
|
os.path.join(normalized_ql_dir, 'src'),
|
|
os.path.join(normalized_ql_dir, 'shell'),
|
|
os.path.join(normalized_ql_dir, 'sample'),
|
|
os.path.join(normalized_ql_dir, 'node_modules'),
|
|
os.path.join(normalized_data_dir, 'config'),
|
|
os.path.join(normalized_data_dir, 'db'),
|
|
]
|
|
|
|
# Allowed write directories - scripts can write here
|
|
allowed_write_paths = [
|
|
os.path.join(normalized_data_dir, 'scripts'),
|
|
os.path.join(normalized_data_dir, 'log'),
|
|
os.path.join(normalized_data_dir, 'repo'),
|
|
os.path.join(normalized_data_dir, 'raw'),
|
|
os.path.join(normalized_ql_dir, '.tmp'),
|
|
'/tmp',
|
|
]
|
|
|
|
# Check if sandboxing is enabled (default: true)
|
|
sandbox_enabled = os.environ.get('QL_DISABLE_SANDBOX') != 'true'
|
|
|
|
def is_path_protected(target_path):
|
|
"""Check if a path is protected from write operations"""
|
|
if not sandbox_enabled:
|
|
return False
|
|
|
|
try:
|
|
# Resolve to absolute path and follow symlinks
|
|
resolved_path = os.path.realpath(target_path)
|
|
|
|
# Check if path is in a protected directory
|
|
for protected_path in protected_paths:
|
|
if resolved_path.startswith(protected_path + os.sep) or resolved_path == protected_path:
|
|
# Check if it's in an allowed subdirectory
|
|
is_in_allowed_path = any(
|
|
resolved_path.startswith(allowed_path + os.sep) or resolved_path == allowed_path
|
|
for allowed_path in allowed_write_paths
|
|
)
|
|
|
|
if not is_in_allowed_path:
|
|
return True
|
|
|
|
# Also check if trying to write inside ql_dir or data_dir without being in allowed paths
|
|
is_in_ql_dir = resolved_path.startswith(normalized_ql_dir + os.sep) or resolved_path == normalized_ql_dir
|
|
is_in_data_dir = resolved_path.startswith(normalized_data_dir + os.sep) or resolved_path == normalized_data_dir
|
|
|
|
if is_in_ql_dir or is_in_data_dir:
|
|
is_in_allowed_path = any(
|
|
resolved_path.startswith(allowed_path + os.sep) or resolved_path == allowed_path
|
|
for allowed_path in allowed_write_paths
|
|
)
|
|
|
|
if not is_in_allowed_path:
|
|
return True
|
|
|
|
return False
|
|
except:
|
|
# If path doesn't exist yet, check parent directory
|
|
parent_path = os.path.dirname(target_path)
|
|
if parent_path != target_path:
|
|
return is_path_protected(parent_path)
|
|
return False
|
|
|
|
def create_security_error(operation, target_path):
|
|
"""Create a security error for unauthorized file operations"""
|
|
return PermissionError(
|
|
f"Security Error: Script attempted to {operation} protected path: {target_path}\n"
|
|
f"Scripts are only allowed to write to: {', '.join(allowed_write_paths)}"
|
|
)
|
|
|
|
# Store original functions
|
|
original_open = builtins.open
|
|
original_os_remove = os.remove
|
|
original_os_unlink = os.unlink
|
|
original_os_rmdir = os.rmdir
|
|
original_os_mkdir = os.mkdir
|
|
original_os_makedirs = os.makedirs
|
|
original_os_rename = os.rename
|
|
original_os_replace = os.replace
|
|
original_os_chmod = os.chmod
|
|
original_os_chown = os.chown if hasattr(os, 'chown') else None
|
|
original_os_link = os.link if hasattr(os, 'link') else None
|
|
original_os_symlink = os.symlink if hasattr(os, 'symlink') else None
|
|
original_os_truncate = os.truncate if hasattr(os, 'truncate') else None
|
|
original_os_utime = os.utime if hasattr(os, 'utime') else None
|
|
|
|
# Wrap open() to check write operations
|
|
def sandboxed_open(file, mode='r', *args, **kwargs):
|
|
"""Wrapped open() that checks for protected paths on write operations"""
|
|
if sandbox_enabled and isinstance(mode, str) and any(m in mode for m in ['w', 'a', 'x', '+']):
|
|
if is_path_protected(file):
|
|
raise create_security_error('open for writing', file)
|
|
return original_open(file, mode, *args, **kwargs)
|
|
|
|
# Wrap os functions
|
|
def sandboxed_remove(path, *args, **kwargs):
|
|
if sandbox_enabled and is_path_protected(path):
|
|
raise create_security_error('remove', path)
|
|
return original_os_remove(path, *args, **kwargs)
|
|
|
|
def sandboxed_unlink(path, *args, **kwargs):
|
|
if sandbox_enabled and is_path_protected(path):
|
|
raise create_security_error('unlink', path)
|
|
return original_os_unlink(path, *args, **kwargs)
|
|
|
|
def sandboxed_rmdir(path, *args, **kwargs):
|
|
if sandbox_enabled and is_path_protected(path):
|
|
raise create_security_error('rmdir', path)
|
|
return original_os_rmdir(path, *args, **kwargs)
|
|
|
|
def sandboxed_mkdir(path, *args, **kwargs):
|
|
if sandbox_enabled and is_path_protected(path):
|
|
raise create_security_error('mkdir', path)
|
|
return original_os_mkdir(path, *args, **kwargs)
|
|
|
|
def sandboxed_makedirs(name, *args, **kwargs):
|
|
if sandbox_enabled and is_path_protected(name):
|
|
raise create_security_error('makedirs', name)
|
|
return original_os_makedirs(name, *args, **kwargs)
|
|
|
|
def sandboxed_rename(src, dst, *args, **kwargs):
|
|
if sandbox_enabled:
|
|
if is_path_protected(src):
|
|
raise create_security_error('rename (source)', src)
|
|
if is_path_protected(dst):
|
|
raise create_security_error('rename (destination)', dst)
|
|
return original_os_rename(src, dst, *args, **kwargs)
|
|
|
|
def sandboxed_replace(src, dst, *args, **kwargs):
|
|
if sandbox_enabled:
|
|
if is_path_protected(src):
|
|
raise create_security_error('replace (source)', src)
|
|
if is_path_protected(dst):
|
|
raise create_security_error('replace (destination)', dst)
|
|
return original_os_replace(src, dst, *args, **kwargs)
|
|
|
|
def sandboxed_chmod(path, *args, **kwargs):
|
|
if sandbox_enabled and is_path_protected(path):
|
|
raise create_security_error('chmod', path)
|
|
return original_os_chmod(path, *args, **kwargs)
|
|
|
|
def sandboxed_chown(path, *args, **kwargs):
|
|
if sandbox_enabled and is_path_protected(path):
|
|
raise create_security_error('chown', path)
|
|
return original_os_chown(path, *args, **kwargs)
|
|
|
|
def sandboxed_link(src, dst, *args, **kwargs):
|
|
if sandbox_enabled:
|
|
if is_path_protected(dst):
|
|
raise create_security_error('link', dst)
|
|
return original_os_link(src, dst, *args, **kwargs)
|
|
|
|
def sandboxed_symlink(src, dst, *args, **kwargs):
|
|
if sandbox_enabled:
|
|
if is_path_protected(dst):
|
|
raise create_security_error('symlink', dst)
|
|
return original_os_symlink(src, dst, *args, **kwargs)
|
|
|
|
def sandboxed_truncate(path, *args, **kwargs):
|
|
if sandbox_enabled and is_path_protected(path):
|
|
raise create_security_error('truncate', path)
|
|
return original_os_truncate(path, *args, **kwargs)
|
|
|
|
def sandboxed_utime(path, *args, **kwargs):
|
|
if sandbox_enabled and is_path_protected(path):
|
|
raise create_security_error('utime', path)
|
|
return original_os_utime(path, *args, **kwargs)
|
|
|
|
# Apply sandbox wrappers
|
|
if sandbox_enabled:
|
|
builtins.open = sandboxed_open
|
|
os.remove = sandboxed_remove
|
|
os.unlink = sandboxed_unlink
|
|
os.rmdir = sandboxed_rmdir
|
|
os.mkdir = sandboxed_mkdir
|
|
os.makedirs = sandboxed_makedirs
|
|
os.rename = sandboxed_rename
|
|
os.replace = sandboxed_replace
|
|
os.chmod = sandboxed_chmod
|
|
if original_os_chown:
|
|
os.chown = sandboxed_chown
|
|
if original_os_link:
|
|
os.link = sandboxed_link
|
|
if original_os_symlink:
|
|
os.symlink = sandboxed_symlink
|
|
if original_os_truncate:
|
|
os.truncate = sandboxed_truncate
|
|
if original_os_utime:
|
|
os.utime = sandboxed_utime
|
|
|
|
# Wrap shutil if it's imported
|
|
try:
|
|
import shutil
|
|
original_shutil_rmtree = shutil.rmtree
|
|
original_shutil_copy = shutil.copy
|
|
original_shutil_copy2 = shutil.copy2
|
|
original_shutil_copytree = shutil.copytree
|
|
original_shutil_move = shutil.move
|
|
|
|
def sandboxed_rmtree(path, *args, **kwargs):
|
|
if is_path_protected(path):
|
|
raise create_security_error('rmtree', path)
|
|
return original_shutil_rmtree(path, *args, **kwargs)
|
|
|
|
def sandboxed_copy(src, dst, *args, **kwargs):
|
|
if is_path_protected(dst):
|
|
raise create_security_error('copy', dst)
|
|
return original_shutil_copy(src, dst, *args, **kwargs)
|
|
|
|
def sandboxed_copy2(src, dst, *args, **kwargs):
|
|
if is_path_protected(dst):
|
|
raise create_security_error('copy2', dst)
|
|
return original_shutil_copy2(src, dst, *args, **kwargs)
|
|
|
|
def sandboxed_copytree(src, dst, *args, **kwargs):
|
|
if is_path_protected(dst):
|
|
raise create_security_error('copytree', dst)
|
|
return original_shutil_copytree(src, dst, *args, **kwargs)
|
|
|
|
def sandboxed_move(src, dst, *args, **kwargs):
|
|
if is_path_protected(src):
|
|
raise create_security_error('move (source)', src)
|
|
if is_path_protected(dst):
|
|
raise create_security_error('move (destination)', dst)
|
|
return original_shutil_move(src, dst, *args, **kwargs)
|
|
|
|
shutil.rmtree = sandboxed_rmtree
|
|
shutil.copy = sandboxed_copy
|
|
shutil.copy2 = sandboxed_copy2
|
|
shutil.copytree = sandboxed_copytree
|
|
shutil.move = sandboxed_move
|
|
except ImportError:
|
|
pass
|
|
|
|
# Wrap pathlib.Path if available
|
|
try:
|
|
original_path_write_text = Path.write_text
|
|
original_path_write_bytes = Path.write_bytes
|
|
original_path_touch = Path.touch
|
|
original_path_mkdir = Path.mkdir
|
|
original_path_rmdir = Path.rmdir
|
|
original_path_unlink = Path.unlink
|
|
original_path_rename = Path.rename
|
|
original_path_replace = Path.replace
|
|
original_path_chmod = Path.chmod
|
|
|
|
def sandboxed_path_write_text(self, *args, **kwargs):
|
|
if is_path_protected(str(self)):
|
|
raise create_security_error('Path.write_text', str(self))
|
|
return original_path_write_text(self, *args, **kwargs)
|
|
|
|
def sandboxed_path_write_bytes(self, *args, **kwargs):
|
|
if is_path_protected(str(self)):
|
|
raise create_security_error('Path.write_bytes', str(self))
|
|
return original_path_write_bytes(self, *args, **kwargs)
|
|
|
|
def sandboxed_path_touch(self, *args, **kwargs):
|
|
if is_path_protected(str(self)):
|
|
raise create_security_error('Path.touch', str(self))
|
|
return original_path_touch(self, *args, **kwargs)
|
|
|
|
def sandboxed_path_mkdir(self, *args, **kwargs):
|
|
if is_path_protected(str(self)):
|
|
raise create_security_error('Path.mkdir', str(self))
|
|
return original_path_mkdir(self, *args, **kwargs)
|
|
|
|
def sandboxed_path_rmdir(self, *args, **kwargs):
|
|
if is_path_protected(str(self)):
|
|
raise create_security_error('Path.rmdir', str(self))
|
|
return original_path_rmdir(self, *args, **kwargs)
|
|
|
|
def sandboxed_path_unlink(self, *args, **kwargs):
|
|
if is_path_protected(str(self)):
|
|
raise create_security_error('Path.unlink', str(self))
|
|
return original_path_unlink(self, *args, **kwargs)
|
|
|
|
def sandboxed_path_rename(self, target, *args, **kwargs):
|
|
if is_path_protected(str(self)):
|
|
raise create_security_error('Path.rename (source)', str(self))
|
|
if is_path_protected(str(target)):
|
|
raise create_security_error('Path.rename (target)', str(target))
|
|
return original_path_rename(self, target, *args, **kwargs)
|
|
|
|
def sandboxed_path_replace(self, target, *args, **kwargs):
|
|
if is_path_protected(str(self)):
|
|
raise create_security_error('Path.replace (source)', str(self))
|
|
if is_path_protected(str(target)):
|
|
raise create_security_error('Path.replace (target)', str(target))
|
|
return original_path_replace(self, target, *args, **kwargs)
|
|
|
|
def sandboxed_path_chmod(self, *args, **kwargs):
|
|
if is_path_protected(str(self)):
|
|
raise create_security_error('Path.chmod', str(self))
|
|
return original_path_chmod(self, *args, **kwargs)
|
|
|
|
Path.write_text = sandboxed_path_write_text
|
|
Path.write_bytes = sandboxed_path_write_bytes
|
|
Path.touch = sandboxed_path_touch
|
|
Path.mkdir = sandboxed_path_mkdir
|
|
Path.rmdir = sandboxed_path_rmdir
|
|
Path.unlink = sandboxed_path_unlink
|
|
Path.rename = sandboxed_path_rename
|
|
Path.replace = sandboxed_path_replace
|
|
Path.chmod = sandboxed_path_chmod
|
|
except:
|
|
pass
|
|
|
|
# Wrap subprocess to prevent sandbox bypass via subprocesses
|
|
try:
|
|
import subprocess
|
|
|
|
# Helper to ensure PYTHONPATH is set for subprocesses
|
|
def ensure_sandbox_env(env=None):
|
|
if env is None:
|
|
env = os.environ.copy()
|
|
else:
|
|
env = env.copy()
|
|
|
|
# Ensure PYTHONPATH includes the sandbox directory
|
|
sandbox_dir = os.path.dirname(__file__)
|
|
if 'PYTHONPATH' not in env:
|
|
env['PYTHONPATH'] = ''
|
|
if sandbox_dir not in env['PYTHONPATH']:
|
|
env['PYTHONPATH'] = f"{sandbox_dir}:{env['PYTHONPATH']}"
|
|
|
|
return env
|
|
|
|
# Store original functions
|
|
original_popen = subprocess.Popen
|
|
original_run = subprocess.run
|
|
original_call = subprocess.call
|
|
original_check_call = subprocess.check_call
|
|
original_check_output = subprocess.check_output
|
|
|
|
# Wrap Popen
|
|
class SandboxedPopen(subprocess.Popen):
|
|
def __init__(self, *args, **kwargs):
|
|
if 'env' in kwargs:
|
|
kwargs['env'] = ensure_sandbox_env(kwargs['env'])
|
|
else:
|
|
kwargs['env'] = ensure_sandbox_env()
|
|
original_popen.__init__(self, *args, **kwargs)
|
|
|
|
subprocess.Popen = SandboxedPopen
|
|
|
|
# Wrap run
|
|
def sandboxed_run(*args, **kwargs):
|
|
if 'env' in kwargs:
|
|
kwargs['env'] = ensure_sandbox_env(kwargs['env'])
|
|
else:
|
|
kwargs['env'] = ensure_sandbox_env()
|
|
return original_run(*args, **kwargs)
|
|
|
|
subprocess.run = sandboxed_run
|
|
|
|
# Wrap call
|
|
def sandboxed_call(*args, **kwargs):
|
|
if 'env' in kwargs:
|
|
kwargs['env'] = ensure_sandbox_env(kwargs['env'])
|
|
else:
|
|
kwargs['env'] = ensure_sandbox_env()
|
|
return original_call(*args, **kwargs)
|
|
|
|
subprocess.call = sandboxed_call
|
|
|
|
# Wrap check_call
|
|
def sandboxed_check_call(*args, **kwargs):
|
|
if 'env' in kwargs:
|
|
kwargs['env'] = ensure_sandbox_env(kwargs['env'])
|
|
else:
|
|
kwargs['env'] = ensure_sandbox_env()
|
|
return original_check_call(*args, **kwargs)
|
|
|
|
subprocess.check_call = sandboxed_check_call
|
|
|
|
# Wrap check_output
|
|
def sandboxed_check_output(*args, **kwargs):
|
|
if 'env' in kwargs:
|
|
kwargs['env'] = ensure_sandbox_env(kwargs['env'])
|
|
else:
|
|
kwargs['env'] = ensure_sandbox_env()
|
|
return original_check_output(*args, **kwargs)
|
|
|
|
subprocess.check_output = sandboxed_check_output
|
|
|
|
except ImportError:
|
|
pass
|
|
|