diff --git a/shell/preload/sandbox.js b/shell/preload/sandbox.js index 6ccca063..7aae2009 100644 --- a/shell/preload/sandbox.js +++ b/shell/preload/sandbox.js @@ -212,7 +212,112 @@ if (sandboxEnabled) { } } -// Prevent requiring the original fs module to bypass sandbox +// Wrap child_process to prevent sandbox bypass via subprocesses +let childProcessWrapped = false; +if (sandboxEnabled) { + // We need to get child_process before wrapping Module.prototype.require + const childProcess = require('child_process'); + const originalSpawn = childProcess.spawn; + const originalExec = childProcess.exec; + const originalExecSync = childProcess.execSync; + const originalExecFile = childProcess.execFile; + const originalExecFileSync = childProcess.execFileSync; + const originalFork = childProcess.fork; + + // Helper to ensure NODE_OPTIONS and PYTHONPATH are set for subprocesses + function ensureSandboxEnv(options = {}) { + const env = { ...process.env, ...options.env }; + + // Ensure NODE_OPTIONS includes the sandbox + const sandboxPreload = path.join(__dirname, 'sandbox.js'); + if (!env.NODE_OPTIONS) { + env.NODE_OPTIONS = ''; + } + if (!env.NODE_OPTIONS.includes(sandboxPreload)) { + env.NODE_OPTIONS = `-r ${sandboxPreload} ${env.NODE_OPTIONS}`.trim(); + } + + // Ensure PYTHONPATH includes the sandbox directory + if (!env.PYTHONPATH) { + env.PYTHONPATH = ''; + } + if (!env.PYTHONPATH.includes(__dirname)) { + env.PYTHONPATH = `${__dirname}:${env.PYTHONPATH}`; + } + + return { ...options, env }; + } + + // Wrap spawn + childProcess.spawn = function(...args) { + if (args[2]) { + args[2] = ensureSandboxEnv(args[2]); + } else if (args.length >= 3) { + args[2] = ensureSandboxEnv({}); + } + return originalSpawn.apply(childProcess, args); + }; + + // Wrap exec + childProcess.exec = function(...args) { + const callback = typeof args[args.length - 1] === 'function' ? args[args.length - 1] : undefined; + const optionsIndex = callback ? args.length - 2 : args.length - 1; + + if (args[optionsIndex] && typeof args[optionsIndex] === 'object') { + args[optionsIndex] = ensureSandboxEnv(args[optionsIndex]); + } else if (optionsIndex > 0) { + args.splice(optionsIndex, 0, ensureSandboxEnv({})); + } + return originalExec.apply(childProcess, args); + }; + + // Wrap execSync + childProcess.execSync = function(...args) { + if (args[1]) { + args[1] = ensureSandboxEnv(args[1]); + } else { + args[1] = ensureSandboxEnv({}); + } + return originalExecSync.apply(childProcess, args); + }; + + // Wrap execFile + childProcess.execFile = function(...args) { + const callback = typeof args[args.length - 1] === 'function' ? args[args.length - 1] : undefined; + const optionsIndex = callback ? args.length - 2 : args.length - 1; + + if (args[optionsIndex] && typeof args[optionsIndex] === 'object') { + args[optionsIndex] = ensureSandboxEnv(args[optionsIndex]); + } else if (optionsIndex > 1) { + args.splice(optionsIndex, 0, ensureSandboxEnv({})); + } + return originalExecFile.apply(childProcess, args); + }; + + // Wrap execFileSync + childProcess.execFileSync = function(...args) { + if (args[2]) { + args[2] = ensureSandboxEnv(args[2]); + } else if (args.length >= 3) { + args[2] = ensureSandboxEnv({}); + } + return originalExecFileSync.apply(childProcess, args); + }; + + // Wrap fork + childProcess.fork = function(...args) { + if (args[2]) { + args[2] = ensureSandboxEnv(args[2]); + } else if (args.length >= 3) { + args[2] = ensureSandboxEnv({}); + } + return originalFork.apply(childProcess, args); + }; + + childProcessWrapped = true; +} + +// Prevent requiring the original fs or child_process modules to bypass sandbox const originalRequire = Module.prototype.require; Module.prototype.require = function(id) { const module = originalRequire.apply(this, arguments); @@ -222,6 +327,9 @@ Module.prototype.require = function(id) { return fs; } + // For child_process, we already wrapped it above, so just return it + // (no need to re-require as that would cause recursion) + return module; }; diff --git a/shell/preload/sandbox.py b/shell/preload/sandbox.py index 5bdce2ce..f28b1707 100644 --- a/shell/preload/sandbox.py +++ b/shell/preload/sandbox.py @@ -324,3 +324,85 @@ if sandbox_enabled: 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 +