Spaces:
Running
Running
File size: 8,400 Bytes
28a08e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | """backend/js_sandbox.py β P20-JS-SANDBOX: Secure JS/TS execution.
Security layers (in order):
1. Blocklist pre-filter (_JS_BLOCKED_RE) β blocks obvious CJS dangerous patterns
2. Deno sandbox (primary, if available): OS-level deny-all for fs/net/env/subprocess
3. Node vm fallback: vm.runInContext in isolated Context (no require/process/fs)
+ --disallow-code-generation-from-strings
Deno detection: $DENO_PATH env var, then /data/deno/bin/deno, /usr/local/bin/deno,
/home/user/.deno/bin/deno, /root/.deno/bin/deno, then PATH lookup.
Why Deno beats blocklist:
- Blocklist bypassed via dynamic require, obfuscation, prototype chain attacks
- Deno: OS-level deny-all β every permission must be explicitly granted with --allow-*
- Node vm: JS-level isolation β sandbox Context has no require/process/fs globals
"""
import os, asyncio, re, logging
from typing import Optional
_logger = logging.getLogger("api.js_sandbox")
# ββ Deno detection βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_DENO_CANDIDATES = [
os.environ.get("DENO_PATH", ""),
"/data/deno/bin/deno",
"/usr/local/bin/deno",
"/home/user/.deno/bin/deno",
"/root/.deno/bin/deno",
"deno", # PATH lookup
]
def _find_deno() -> Optional[str]:
"""Return path to Deno binary if available, else None."""
import shutil
for candidate in _DENO_CANDIDATES:
if not candidate:
continue
if os.path.isabs(candidate):
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
return candidate
else:
found = shutil.which(candidate)
if found:
return found
return None
_DENO_BIN: Optional[str] = _find_deno()
_logger.info("[js_sandbox] Deno: %s", _DENO_BIN or "NOT FOUND β using Node vm fallback")
# ββ JS/TS blocklist (defense-in-depth β belt, Deno/vm are suspenders) βββββ
# Kept even when Deno is available: O(1) pre-filter before any subprocess spawn.
_JS_BLOCKED_RE = re.compile(
r'\brequire\s*\(\s*[\'"]child_process[\'"]\s*\)'
r'|\bprocess\.env\b'
r'|\bfs\s*\.\s*(read|write|unlink|rm)'
r'|\bexecSync\b|\bspawnSync\b'
r'|\brequire\s*\(\s*[\'"](?:net|http|https|dgram|cluster|worker_threads)[\'"]'
r'\s*\)',
re.IGNORECASE,
)
# ββ Node vm runner script (written to tmpdir per-call) βββββββββββββββββββββ
# Reads user code from argv[2]. vm.createContext: no require/process/fs globals.
_NODE_VM_RUNNER = r"""'use strict';
const vm = require('vm');
const fs = require('fs');
const code = fs.readFileSync(process.argv[2], 'utf8');
const sandbox = {
console: {
log: (...a) => process.stdout.write(a.map(String).join(' ') + '\n'),
error: (...a) => process.stderr.write(a.map(String).join(' ') + '\n'),
warn: (...a) => process.stderr.write('[warn] ' + a.map(String).join(' ') + '\n'),
info: (...a) => process.stdout.write('[info] ' + a.map(String).join(' ') + '\n'),
},
Math, JSON, Date, Array, Object, String, Number, Boolean, RegExp,
Error, TypeError, RangeError, SyntaxError, ReferenceError,
Map, Set, WeakMap, WeakSet, Symbol, BigInt, Promise,
parseInt, parseFloat, isNaN, isFinite, encodeURIComponent, decodeURIComponent,
};
const ctx = vm.createContext(sandbox);
try {
vm.runInContext(code, ctx, { timeout: 10000, filename: 'sandbox.js' });
} catch (e) {
process.stderr.write(String(e) + '\n');
process.exit(1);
}
"""
async def run_js_sandbox(
code: str,
lang: str,
tmpdir: str,
timeout: float = 12.0,
preexec_fn=None,
safe_env: Optional[dict] = None,
) -> dict:
"""Execute JS/TS code in a sandbox. Returns dict with stdout, stderr, returncode.
Security: blocklist pre-filter (belt) β Deno deny-all sandbox (primary)
β Node vm.createContext (fallback).
TypeScript: supported natively by Deno only; Node vm fallback refuses TS.
"""
lang_norm = lang.lower()
is_ts = lang_norm in ("typescript", "ts")
# Belt: fast pre-filter β catches obvious CJS/process/fs dangerous patterns
bad = _JS_BLOCKED_RE.search(code)
if bad:
return {
"stdout": "",
"stderr": f"js_blocked: pattern '{bad.group()}' non permesso nel sandbox",
"returncode": 1,
}
if _DENO_BIN:
return await _run_with_deno(code, is_ts, tmpdir, timeout, preexec_fn, safe_env or {})
if is_ts:
return {
"stdout": "",
"stderr": (
"TypeScript sandbox: Deno non disponibile β TS non supportato nel fallback Node vm. "
"Installa Deno (https://deno.land) o invia codice in JavaScript."
),
"returncode": 1,
}
return await _run_with_node_vm(code, tmpdir, timeout, preexec_fn, safe_env or {})
async def _run_with_deno(
code: str, is_ts: bool, tmpdir: str,
timeout: float, preexec_fn, env: dict,
) -> dict:
"""Primary sandbox: Deno run β default deny-all (no --allow-* flags passed).
Deno 1.x+: omitting all --allow-* flags = deny-all for fs/net/env/subprocess.
--no-prompt: never prompt interactively for permissions (would hang).
"""
ext = ".ts" if is_ts else ".js"
fname = os.path.join(tmpdir, f"sandbox{ext}")
with open(fname, "w") as fh:
fh.write(code)
# No --allow-* = deny-all by default (all Deno versions).
cmd = [_DENO_BIN, "run", "--no-prompt", fname]
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=tmpdir,
env=env,
preexec_fn=preexec_fn,
)
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
except asyncio.TimeoutError:
try:
proc.kill()
except Exception:
pass
await proc.wait()
return {"stdout": "", "stderr": f"\u26a0\ufe0f Timeout {timeout:.0f}s (Deno sandbox)", "returncode": -1}
return {
"stdout": stdout.decode("utf-8", errors="replace")[:8000],
"stderr": stderr.decode("utf-8", errors="replace")[:4000],
"returncode": proc.returncode,
}
except Exception as exc:
_logger.warning("[js_sandbox/deno] spawn error: %s", exc)
return {"stdout": "", "stderr": str(exc)[:300], "returncode": 1}
async def _run_with_node_vm(
code: str, tmpdir: str,
timeout: float, preexec_fn, env: dict,
) -> dict:
"""Fallback sandbox: Node.js vm.createContext β no require/process/fs in Context.
Not an OS-level sandbox (prototype chain escapes exist in theory), but combined
with the blocklist pre-filter provides solid defense-in-depth for most threats.
"""
code_file = os.path.join(tmpdir, "user_code.js")
runner_file = os.path.join(tmpdir, "_vm_runner.js")
with open(code_file, "w") as fh:
fh.write(code)
with open(runner_file, "w") as fh:
fh.write(_NODE_VM_RUNNER)
cmd = [
"node",
"--disallow-code-generation-from-strings",
runner_file,
code_file,
]
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=tmpdir,
env=env,
preexec_fn=preexec_fn,
)
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
except asyncio.TimeoutError:
try:
proc.kill()
except Exception:
pass
await proc.wait()
return {"stdout": "", "stderr": f"\u26a0\ufe0f Timeout {timeout:.0f}s (Node vm sandbox)", "returncode": -1}
return {
"stdout": stdout.decode("utf-8", errors="replace")[:8000],
"stderr": stderr.decode("utf-8", errors="replace")[:4000],
"returncode": proc.returncode,
}
except Exception as exc:
_logger.warning("[js_sandbox/node-vm] spawn error: %s", exc)
return {"stdout": "", "stderr": str(exc)[:300], "returncode": 1}
|