import { NextRequest } from 'next/server'; import { spawn } from 'child_process'; import { writeFile, unlink, mkdir, readFile, readdir } from 'fs/promises'; import { join } from 'path'; import { tmpdir } from 'os'; import { randomUUID } from 'crypto'; export const maxDuration = 60; interface ExecuteRequestBody { code: string; timeout?: number; } interface ExecutionResult { success: boolean; output: string; error: string; executionTime: number; hasCircuitOutput: boolean; images?: string[]; // Base64 encoded images } const DANGEROUS_PATTERNS = [ /os\.environ/, /environ\[/, /getenv\s*\(/, // Dangerous modules /\bctypes\b/, /\bpickle\b/, /\bmarshal\b/, /\bshelve\b/, /\bcommands\b/, /\bpty\b/, /\bpexpect\b/, // System/shell access /\bos\.system\b/, /\bos\.popen\b/, /\bos\.spawn/, /\bos\.exec/, /\bos\.fork\b/, /\bsubprocess\b/, /\bcommands\b/, // File system attacks outside sandbox /open\s*\(\s*['"]\s*\/etc/, /open\s*\(\s*['"]\s*\/proc/, /open\s*\(\s*['"]\s*\/sys/, /open\s*\(\s*['"]\s*\/dev/, /open\s*\(\s*['"]\s*\/var/, /open\s*\(\s*['"]\s*\/root/, /open\s*\(\s*['"]\s*\/home/, /open\s*\(\s*['"]\s*\/tmp/, /open\s*\(\s*['"]\s*\.env/, /open\s*\(\s*['"]\s*\.\.\//, // Path traversal /open\s*\(\s*f?['"]\s*\{/, // f-string with path // Network access /\bsocket\b/, /\burllib\b/, /\brequests\b/, /\bhttpx\b/, /\baiohttp\b/, /\bhttp\.client\b/, /\bftplib\b/, /\bsmtplib\b/, /\btelnetlib\b/, /\bparamiko\b/, // Code execution /\beval\s*\(/, /\bexec\s*\(/, /\bcompile\s*\(/, /\b__import__\b/, /\bimportlib\b/, /\bbuiltins\b/, /\bglobals\s*\(\s*\)/, /\blocals\s*\(\s*\)/, /\bgetattr\s*\([^,]+,\s*['"]/, // getattr with string /\bsetattr\s*\(/, /\bdelattr\s*\(/, // Class/object manipulation for sandbox escape /\b__class__\b/, /\b__bases__\b/, /\b__subclasses__\b/, /\b__mro__\b/, /\b__globals__\b/, /\b__code__\b/, /\b__reduce__\b/, /\b__getstate__\b/, /\b__setstate__\b/, // Multiprocessing (can be used to bypass restrictions) /\bmultiprocessing\b/, /\bthreading\b/, /\bconcurrent\b/, /\basyncio\.subprocess/, ]; const ALLOWED_PATTERNS = [ /from qiskit/, /import qiskit/, /from numpy/, /import numpy/, /from scipy/, /import scipy/, /from matplotlib/, /import matplotlib/, ]; function validateCode(code: string): { valid: boolean; error?: string } { const codeWithoutComments = code .replace(/#.*$/gm, '') // Remove single-line comments .replace(/'''[\s\S]*?'''/g, '') // Remove triple-single-quote strings .replace(/"""[\s\S]*?"""/g, ''); // Remove triple-double-quote strings for (const pattern of DANGEROUS_PATTERNS) { if (pattern.test(codeWithoutComments)) { return { valid: false, error: `Security error: Potentially dangerous code pattern detected. For security reasons, certain operations are not allowed in the sandbox.` }; } } return { valid: true }; } const createSafetyWrapper = (figureDir: string) => ` import sys import io import os import warnings import builtins from contextlib import redirect_stdout, redirect_stderr # Suppress warnings for cleaner output warnings.filterwarnings('ignore') # Setup figure capture directory _FIGURE_DIR = "${figureDir.replace(/\\/g, '\\\\')}" _figure_counter = [0] # ============================================ # SECURITY SANDBOX SETUP (Second Line of Defense) # Primary security is pattern detection + clean environment # ============================================ # Block dangerous system operations os.system = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.system not allowed in sandbox")) os.popen = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.popen not allowed in sandbox")) if hasattr(os, 'spawn'): os.spawn = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawn not allowed")) if hasattr(os, 'spawnl'): os.spawnl = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnl not allowed")) if hasattr(os, 'spawnle'): os.spawnle = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnle not allowed")) if hasattr(os, 'spawnlp'): os.spawnlp = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnlp not allowed")) if hasattr(os, 'spawnlpe'): os.spawnlpe = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnlpe not allowed")) if hasattr(os, 'spawnv'): os.spawnv = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnv not allowed")) if hasattr(os, 'spawnve'): os.spawnve = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnve not allowed")) if hasattr(os, 'spawnvp'): os.spawnvp = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnvp not allowed")) if hasattr(os, 'spawnvpe'): os.spawnvpe = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnvpe not allowed")) if hasattr(os, 'execl'): os.execl = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execl not allowed")) if hasattr(os, 'execle'): os.execle = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execle not allowed")) if hasattr(os, 'execlp'): os.execlp = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execlp not allowed")) if hasattr(os, 'execlpe'): os.execlpe = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execlpe not allowed")) if hasattr(os, 'execv'): os.execv = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execv not allowed")) if hasattr(os, 'execve'): os.execve = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execve not allowed")) if hasattr(os, 'execvp'): os.execvp = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execvp not allowed")) if hasattr(os, 'execvpe'): os.execvpe = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execvpe not allowed")) if hasattr(os, 'fork'): os.fork = lambda: (_ for _ in ()).throw(PermissionError("os.fork not allowed")) if hasattr(os, 'forkpty'): os.forkpty = lambda: (_ for _ in ()).throw(PermissionError("os.forkpty not allowed")) if hasattr(os, 'killpg'): os.killpg = lambda *args: (_ for _ in ()).throw(PermissionError("os.killpg not allowed")) if hasattr(os, 'kill'): os.kill = lambda *args: (_ for _ in ()).throw(PermissionError("os.kill not allowed")) # Block subprocess module try: import subprocess subprocess.run = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) subprocess.call = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) subprocess.check_call = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) subprocess.check_output = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) subprocess.Popen = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) subprocess.getoutput = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) subprocess.getstatusoutput = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) except ImportError: pass # Create restricted open function to block access to sensitive files _original_open = builtins.open _ALLOWED_PATHS = [_FIGURE_DIR, '/tmp/quantum-sandbox'] def _restricted_open(file, mode='r', *args, **kwargs): """Restricted open that blocks access to sensitive files""" if isinstance(file, (str, bytes)): file_str = file if isinstance(file, str) else file.decode() # Don't block relative paths that are needed for library operation if file_str.startswith('/'): file_str_lower = file_str.lower() # Block reading system sensitive paths blocked_prefixes = ['/etc/passwd', '/etc/shadow', '/proc/self', '/proc/1'] for prefix in blocked_prefixes: if file_str_lower.startswith(prefix): raise PermissionError(f"Access to {prefix} is not allowed in sandbox") # Block reading obvious secrets blocked_patterns = ['.env.local', '.env.', 'secrets', 'credentials', 'private_key'] for pattern in blocked_patterns: if pattern in file_str_lower: raise PermissionError(f"Access to files matching '{pattern}' is not allowed in sandbox") return _original_open(file, mode, *args, **kwargs) builtins.open = _restricted_open # ============================================ # END SECURITY SANDBOX SETUP # ============================================ # Capture all output _stdout_capture = io.StringIO() _stderr_capture = io.StringIO() # Setup matplotlib figure capture try: import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt _original_show = plt.show _original_savefig = plt.savefig def _capture_show(*args, **kwargs): """Capture plt.show() calls and save figures""" figs = [plt.figure(i) for i in plt.get_fignums()] for fig in figs: _figure_counter[0] += 1 filepath = os.path.join(_FIGURE_DIR, f"figure_{_figure_counter[0]}.png") fig.savefig(filepath, format='png', dpi=150, bbox_inches='tight', facecolor='#18181b', edgecolor='none', transparent=False) plt.close('all') def _capture_savefig(fname, *args, **kwargs): """Capture savefig calls""" _figure_counter[0] += 1 filepath = os.path.join(_FIGURE_DIR, f"figure_{_figure_counter[0]}.png") kwargs_copy = dict(kwargs) kwargs_copy['format'] = 'png' kwargs_copy['dpi'] = kwargs_copy.get('dpi', 150) kwargs_copy['bbox_inches'] = kwargs_copy.get('bbox_inches', 'tight') kwargs_copy['facecolor'] = kwargs_copy.get('facecolor', '#18181b') _original_savefig(filepath, **kwargs_copy) plt.show = _capture_show plt.savefig = _capture_savefig # Also capture Qiskit circuit.draw() with mpl output try: from qiskit import QuantumCircuit _original_draw = QuantumCircuit.draw def _capture_draw(self, output=None, **kwargs): result = _original_draw(self, output=output, **kwargs) if output == 'mpl' and result is not None: _figure_counter[0] += 1 filepath = os.path.join(_FIGURE_DIR, f"figure_{_figure_counter[0]}.png") result.savefig(filepath, format='png', dpi=150, bbox_inches='tight', facecolor='#18181b', edgecolor='none') plt.close(result) return result QuantumCircuit.draw = _capture_draw except ImportError: pass except ImportError: pass # Now execute the user code with output capture with redirect_stdout(_stdout_capture), redirect_stderr(_stderr_capture): try: exec(compile(''' __USER_CODE__ ''', '', 'exec'), {'__builtins__': builtins, '__name__': '__main__'}) except Exception as e: print(f"{type(e).__name__}: {e}", file=sys.stderr) # Final figure capture - save any remaining open figures try: import matplotlib.pyplot as plt figs = [plt.figure(i) for i in plt.get_fignums()] for fig in figs: _figure_counter[0] += 1 filepath = os.path.join(_FIGURE_DIR, f"figure_{_figure_counter[0]}.png") fig.savefig(filepath, format='png', dpi=150, bbox_inches='tight', facecolor='#18181b', edgecolor='none', transparent=False) plt.close('all') except: pass # Print captured output _stdout_result = _stdout_capture.getvalue() _stderr_result = _stderr_capture.getvalue() if _stdout_result: print(_stdout_result, end='') if _stderr_result: print(_stderr_result, end='', file=sys.stderr) `; function createSafeCode(userCode: string, figureDir: string): string { const escapedCode = userCode .replace(/\\/g, '\\\\') .replace(/'''/g, "\\'\\'\\'"); return createSafetyWrapper(figureDir).replace('__USER_CODE__', escapedCode); } function getSafeEnv(): Record { const env: Record = { PATH: '/usr/bin:/bin:/usr/local/bin', HOME: '/tmp', PYTHONUNBUFFERED: '1', MPLBACKEND: 'Agg', MallocStackLogging: '0', MallocNanoZone: '0', LANG: 'en_US.UTF-8', LC_ALL: 'en_US.UTF-8', }; if (process.env.PYTHON_PATH) { env.PYTHON_PATH = process.env.PYTHON_PATH; } return env; } function detectCircuitOutput(code: string, output: string): boolean { // Check if the code likely produces circuit visualization const circuitPatterns = [ /\.draw\(/, /circuit_drawer/, /plot_histogram/, /plot_bloch/, /\.decompose\(\)/, /print.*circuit/i, ]; const outputPatterns = [ /[┌─┬┐│├┼┤└┴┘═║╔╗╚╝]/, // ASCII circuit characters /q\d*.*[─┤├]/, // Qubit lines /[HXYZTSRx].*├/, // Gate symbols ]; const hasCircuitCode = circuitPatterns.some(p => p.test(code)); const hasCircuitOutput = outputPatterns.some(p => p.test(output)); return hasCircuitCode || hasCircuitOutput; } async function collectFigures(figureDir: string): Promise { const images: string[] = []; try { const files = await readdir(figureDir); const pngFiles = files.filter(f => f.endsWith('.png')).sort(); for (const file of pngFiles) { const filepath = join(figureDir, file); const data = await readFile(filepath); images.push(data.toString('base64')); // Clean up the file await unlink(filepath).catch(() => { }); } } catch { // Directory might not exist or be empty } return images; } async function executeCode(code: string, timeout: number): Promise { const startTime = Date.now(); const execId = randomUUID(); const tempDir = join(tmpdir(), 'quantum-sandbox'); const figureDir = join(tempDir, `figures_${execId}`); const tempFile = join(tempDir, `exec_${execId}.py`); try { // Ensure temp directories exist await mkdir(tempDir, { recursive: true }); await mkdir(figureDir, { recursive: true }); // Create safe wrapped code const safeCode = createSafeCode(code, figureDir); await writeFile(tempFile, safeCode, 'utf-8'); return await new Promise(async (resolve) => { let stdout = ''; let stderr = ''; let killed = false; // Use the PYTHON_PATH environment variable if set, otherwise default to python3 const pythonPath = process.env.PYTHON_PATH || 'python3'; const pythonProcess = spawn(pythonPath, [tempFile], { timeout: timeout * 1000, env: getSafeEnv() as NodeJS.ProcessEnv, // Use minimal safe environment, no secrets cwd: tempDir, // Run in isolated temp directory }); pythonProcess.stdout.on('data', (data: Buffer) => { stdout += data.toString(); }); pythonProcess.stderr.on('data', (data: Buffer) => { stderr += data.toString(); }); const timeoutId = setTimeout(() => { killed = true; pythonProcess.kill('SIGKILL'); }, timeout * 1000); pythonProcess.on('close', async (exitCode: number | null) => { clearTimeout(timeoutId); const executionTime = Date.now() - startTime; // Collect any generated figures const images = await collectFigures(figureDir); if (killed) { resolve({ success: false, output: stdout, error: `Execution timeout (>${timeout}s). The code took too long to execute.`, executionTime, hasCircuitOutput: false, images, }); return; } // Clean up stderr from common warnings const cleanStderr = stderr .split('\n') .filter(line => !line.includes('UserWarning') && !line.includes('DeprecationWarning') && !line.includes('FutureWarning')) .join('\n') .trim(); const success = exitCode === 0 && !cleanStderr; resolve({ success, output: stdout.trim(), error: cleanStderr, executionTime, hasCircuitOutput: detectCircuitOutput(code, stdout), images, }); }); pythonProcess.on('error', (err: Error) => { clearTimeout(timeoutId); resolve({ success: false, output: '', error: `Failed to start Python: ${err.message}`, executionTime: Date.now() - startTime, hasCircuitOutput: false, images: [], }); }); }); } finally { // Clean up temp file and figure directory try { await unlink(tempFile); } catch { // Ignore cleanup errors } try { // Clean up figure directory const files = await readdir(figureDir).catch(() => []); for (const file of files) { await unlink(join(figureDir, file)).catch(() => { }); } await unlink(figureDir).catch(() => { }); } catch { // Ignore cleanup errors } } } export async function POST(request: NextRequest) { try { const body: ExecuteRequestBody = await request.json(); const { code, timeout = 30 } = body; if (!code || typeof code !== 'string') { return new Response( JSON.stringify({ error: 'Invalid request: code string required' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } // Limit code length if (code.length > 50000) { return new Response( JSON.stringify({ error: 'Code too long (max 50KB)' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } // Validate code for dangerous patterns (first line of defense) const validation = validateCode(code); if (!validation.valid) { return new Response( JSON.stringify({ success: false, output: '', error: validation.error, executionTime: 0, hasCircuitOutput: false, images: [], }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } // Limit timeout const safeTimeout = Math.min(Math.max(timeout, 5), 60); const result = await executeCode(code, safeTimeout); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' }, }); } catch (error) { console.error('Execute API error:', error); return new Response( JSON.stringify({ success: false, output: '', error: error instanceof Error ? error.message : 'Execution failed', executionTime: 0, hasCircuitOutput: false, images: [], }), { status: 500, headers: { 'Content-Type': 'application/json' } } ); } }