mirror of
https://github.com/whyour/qinglong.git
synced 2025-12-16 17:35:37 +08:00
Implement filesystem sandbox for Node.js and Python scripts
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
parent
3c2d782ec8
commit
5267cd03e0
233
shell/preload/sandbox.js
Normal file
233
shell/preload/sandbox.js
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
const Module = require('module');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Get the QL_DIR and data directory paths
|
||||||
|
const qlDir = process.env.QL_DIR || path.join(__dirname, '../../');
|
||||||
|
let dataDir = process.env.QL_DATA_DIR || path.join(qlDir, 'data');
|
||||||
|
|
||||||
|
// Remove trailing slash if present
|
||||||
|
dataDir = dataDir.replace(/\/$/, '');
|
||||||
|
|
||||||
|
// Normalize paths to avoid bypassing with relative paths or symlinks
|
||||||
|
const normalizedQlDir = fs.existsSync(qlDir) ? fs.realpathSync(qlDir) : path.resolve(qlDir);
|
||||||
|
const normalizedDataDir = fs.existsSync(dataDir) ? fs.realpathSync(dataDir) : path.resolve(dataDir);
|
||||||
|
|
||||||
|
// Protected directories - no write access allowed
|
||||||
|
const protectedPaths = [
|
||||||
|
path.join(normalizedQlDir, 'back'),
|
||||||
|
path.join(normalizedQlDir, 'src'),
|
||||||
|
path.join(normalizedQlDir, 'shell'),
|
||||||
|
path.join(normalizedQlDir, 'sample'),
|
||||||
|
path.join(normalizedQlDir, 'node_modules'),
|
||||||
|
path.join(normalizedDataDir, 'config'),
|
||||||
|
path.join(normalizedDataDir, 'db'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Allowed write directories - scripts can write here
|
||||||
|
const allowedWritePaths = [
|
||||||
|
path.join(normalizedDataDir, 'scripts'),
|
||||||
|
path.join(normalizedDataDir, 'log'),
|
||||||
|
path.join(normalizedDataDir, 'repo'),
|
||||||
|
path.join(normalizedDataDir, 'raw'),
|
||||||
|
path.join(normalizedQlDir, '.tmp'),
|
||||||
|
'/tmp',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if sandboxing is enabled (default: true)
|
||||||
|
const sandboxEnabled = process.env.QL_DISABLE_SANDBOX !== 'true';
|
||||||
|
|
||||||
|
function isPathProtected(targetPath) {
|
||||||
|
if (!sandboxEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Resolve to absolute path and follow symlinks
|
||||||
|
const resolvedPath = fs.realpathSync.native ?
|
||||||
|
fs.realpathSync.native(targetPath) :
|
||||||
|
path.resolve(targetPath);
|
||||||
|
|
||||||
|
// Check if path is in a protected directory
|
||||||
|
for (const protectedPath of protectedPaths) {
|
||||||
|
if (resolvedPath.startsWith(protectedPath + path.sep) || resolvedPath === protectedPath) {
|
||||||
|
// Check if it's in an allowed subdirectory
|
||||||
|
const isInAllowedPath = allowedWritePaths.some(allowedPath =>
|
||||||
|
resolvedPath.startsWith(allowedPath + path.sep) || resolvedPath === allowedPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isInAllowedPath) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check if trying to write outside data/scripts without being in allowed paths
|
||||||
|
const isInQlDir = resolvedPath.startsWith(normalizedQlDir + path.sep) || resolvedPath === normalizedQlDir;
|
||||||
|
const isInDataDir = resolvedPath.startsWith(normalizedDataDir + path.sep) || resolvedPath === normalizedDataDir;
|
||||||
|
|
||||||
|
if (isInQlDir || isInDataDir) {
|
||||||
|
const isInAllowedPath = allowedWritePaths.some(allowedPath =>
|
||||||
|
resolvedPath.startsWith(allowedPath + path.sep) || resolvedPath === allowedPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isInAllowedPath) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
// If path doesn't exist yet, check parent directory
|
||||||
|
const parentPath = path.dirname(targetPath);
|
||||||
|
if (parentPath !== targetPath) {
|
||||||
|
return isPathProtected(parentPath);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSecurityError(operation, targetPath) {
|
||||||
|
const err = new Error(
|
||||||
|
`Security Error: Script attempted to ${operation} protected path: ${targetPath}\n` +
|
||||||
|
`Scripts are only allowed to write to: ${allowedWritePaths.join(', ')}`
|
||||||
|
);
|
||||||
|
err.code = 'EACCES';
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store original fs methods
|
||||||
|
const originalFS = {};
|
||||||
|
const writeOperations = [
|
||||||
|
'writeFile', 'writeFileSync',
|
||||||
|
'appendFile', 'appendFileSync',
|
||||||
|
'mkdir', 'mkdirSync',
|
||||||
|
'rmdir', 'rmdirSync',
|
||||||
|
'unlink', 'unlinkSync',
|
||||||
|
'rm', 'rmSync',
|
||||||
|
'rename', 'renameSync',
|
||||||
|
'copyFile', 'copyFileSync',
|
||||||
|
'chmod', 'chmodSync',
|
||||||
|
'chown', 'chownSync',
|
||||||
|
'link', 'linkSync',
|
||||||
|
'symlink', 'symlinkSync',
|
||||||
|
'truncate', 'truncateSync',
|
||||||
|
'utimes', 'utimesSync',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Wrap fs methods
|
||||||
|
for (const method of writeOperations) {
|
||||||
|
if (fs[method]) {
|
||||||
|
originalFS[method] = fs[method];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapFsMethod(method, isSync) {
|
||||||
|
return function(...args) {
|
||||||
|
const targetPath = args[0];
|
||||||
|
|
||||||
|
if (isPathProtected(targetPath)) {
|
||||||
|
const err = createSecurityError(method, targetPath);
|
||||||
|
if (isSync) {
|
||||||
|
throw err;
|
||||||
|
} else {
|
||||||
|
const callback = args[args.length - 1];
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
process.nextTick(() => callback(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For rename/copy operations, check destination too
|
||||||
|
if ((method.startsWith('rename') || method.startsWith('copy')) && args[1]) {
|
||||||
|
if (isPathProtected(args[1])) {
|
||||||
|
const err = createSecurityError(method, args[1]);
|
||||||
|
if (isSync) {
|
||||||
|
throw err;
|
||||||
|
} else {
|
||||||
|
const callback = args[args.length - 1];
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
process.nextTick(() => callback(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalFS[method].apply(fs, args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply wrappers
|
||||||
|
if (sandboxEnabled) {
|
||||||
|
for (const method of writeOperations) {
|
||||||
|
if (fs[method]) {
|
||||||
|
const isSync = method.endsWith('Sync');
|
||||||
|
fs[method] = wrapFsMethod(method, isSync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap createWriteStream
|
||||||
|
originalFS.createWriteStream = fs.createWriteStream;
|
||||||
|
fs.createWriteStream = function(targetPath, options) {
|
||||||
|
if (isPathProtected(targetPath)) {
|
||||||
|
throw createSecurityError('createWriteStream', targetPath);
|
||||||
|
}
|
||||||
|
return originalFS.createWriteStream.call(fs, targetPath, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wrap promises API if it exists
|
||||||
|
if (fs.promises) {
|
||||||
|
const promisesOriginal = {};
|
||||||
|
const promisesMethods = [
|
||||||
|
'writeFile', 'appendFile', 'mkdir', 'rmdir', 'unlink', 'rm',
|
||||||
|
'rename', 'copyFile', 'chmod', 'chown', 'link', 'symlink',
|
||||||
|
'truncate', 'utimes',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const method of promisesMethods) {
|
||||||
|
if (fs.promises[method]) {
|
||||||
|
promisesOriginal[method] = fs.promises[method];
|
||||||
|
fs.promises[method] = async function(...args) {
|
||||||
|
const targetPath = args[0];
|
||||||
|
|
||||||
|
if (isPathProtected(targetPath)) {
|
||||||
|
throw createSecurityError(method, targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For rename/copy operations, check destination too
|
||||||
|
if ((method === 'rename' || method === 'copyFile') && args[1]) {
|
||||||
|
if (isPathProtected(args[1])) {
|
||||||
|
throw createSecurityError(method, args[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return promisesOriginal[method].apply(fs.promises, args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent requiring the original fs module to bypass sandbox
|
||||||
|
const originalRequire = Module.prototype.require;
|
||||||
|
Module.prototype.require = function(id) {
|
||||||
|
const module = originalRequire.apply(this, arguments);
|
||||||
|
|
||||||
|
// Return wrapped fs module
|
||||||
|
if (id === 'fs' || id === 'node:fs') {
|
||||||
|
return fs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return module;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sandboxEnabled,
|
||||||
|
isPathProtected,
|
||||||
|
protectedPaths,
|
||||||
|
allowedWritePaths,
|
||||||
|
};
|
||||||
326
shell/preload/sandbox.py
Normal file
326
shell/preload/sandbox.py
Normal file
|
|
@ -0,0 +1,326 @@
|
||||||
|
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
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Load sandbox first to protect filesystem
|
||||||
|
require('./sandbox.js');
|
||||||
|
|
||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
const client = require('./client.js');
|
const client = require('./client.js');
|
||||||
require(`./env.js`);
|
require(`./env.js`);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
# Load sandbox first to protect filesystem
|
||||||
|
import sandbox
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
|
||||||
43
shell/preload/test_sandbox_direct.js
Normal file
43
shell/preload/test_sandbox_direct.js
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Minimal test of sandbox module
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Set up environment
|
||||||
|
process.env.QL_DIR = path.join(__dirname, '../..');
|
||||||
|
process.env.QL_DATA_DIR = path.join(__dirname, '../../data');
|
||||||
|
|
||||||
|
// Load sandbox
|
||||||
|
require('./sandbox.js');
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
console.log("Testing filesystem sandbox...\n");
|
||||||
|
|
||||||
|
// Test 1: Try to write to config directory (should fail)
|
||||||
|
console.log("Test 1: Attempting to write to config/test.txt (should fail)...");
|
||||||
|
try {
|
||||||
|
const configPath = path.join(__dirname, '../../data/config/test.txt');
|
||||||
|
fs.writeFileSync(configPath, 'test');
|
||||||
|
console.log("❌ FAILED: Was able to write to protected config directory!");
|
||||||
|
process.exit(1);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'EACCES' && error.message.includes('Security Error')) {
|
||||||
|
console.log("✅ PASSED: Correctly blocked write to config directory");
|
||||||
|
console.log("Error message:", error.message.split('\n')[0]);
|
||||||
|
} else {
|
||||||
|
console.log("❓ UNEXPECTED ERROR:", error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Write to scripts directory (should succeed)
|
||||||
|
console.log("\nTest 2: Attempting to write to scripts directory (should succeed)...");
|
||||||
|
try {
|
||||||
|
const scriptsPath = path.join(__dirname, '../../data/scripts/test_output.txt');
|
||||||
|
fs.writeFileSync(scriptsPath, 'This is a test file');
|
||||||
|
console.log("✅ PASSED: Successfully wrote to scripts directory");
|
||||||
|
fs.unlinkSync(scriptsPath);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("❌ FAILED: Could not write to allowed scripts directory:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n✅ Basic sandbox tests passed!");
|
||||||
50
shell/preload/test_sandbox_direct.py
Normal file
50
shell/preload/test_sandbox_direct.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Minimal test of Python sandbox module
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Set up environment
|
||||||
|
script_dir = Path(__file__).parent.resolve()
|
||||||
|
ql_dir = script_dir.parent.parent
|
||||||
|
os.environ['QL_DIR'] = str(ql_dir)
|
||||||
|
os.environ['QL_DATA_DIR'] = str(ql_dir / 'data')
|
||||||
|
|
||||||
|
# Add preload to path
|
||||||
|
sys.path.insert(0, str(script_dir))
|
||||||
|
|
||||||
|
# Load sandbox
|
||||||
|
import sandbox
|
||||||
|
|
||||||
|
print("Testing Python filesystem sandbox...\n")
|
||||||
|
|
||||||
|
# Test 1: Try to write to config directory (should fail)
|
||||||
|
print("Test 1: Attempting to write to config/test.txt (should fail)...")
|
||||||
|
try:
|
||||||
|
config_path = ql_dir / 'data' / 'config' / 'test.txt'
|
||||||
|
with open(config_path, 'w') as f:
|
||||||
|
f.write('test')
|
||||||
|
print("❌ FAILED: Was able to write to protected config directory!")
|
||||||
|
sys.exit(1)
|
||||||
|
except PermissionError as e:
|
||||||
|
if 'Security Error' in str(e):
|
||||||
|
print("✅ PASSED: Correctly blocked write to config directory")
|
||||||
|
print(f"Error message: {str(e).split(chr(10))[0]}")
|
||||||
|
else:
|
||||||
|
print(f"❓ UNEXPECTED ERROR: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❓ UNEXPECTED ERROR: {e}")
|
||||||
|
|
||||||
|
# Test 2: Write to scripts directory (should succeed)
|
||||||
|
print("\nTest 2: Attempting to write to scripts directory (should succeed)...")
|
||||||
|
try:
|
||||||
|
scripts_path = ql_dir / 'data' / 'scripts' / 'test_output.txt'
|
||||||
|
with open(scripts_path, 'w') as f:
|
||||||
|
f.write('This is a test file')
|
||||||
|
print("✅ PASSED: Successfully wrote to scripts directory")
|
||||||
|
os.remove(scripts_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ FAILED: Could not write to allowed scripts directory: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("\n✅ Basic Python sandbox tests passed!")
|
||||||
Loading…
Reference in New Issue
Block a user