Terminal / js_sandbox.py
Baida-A
Initial clean deploy (Reverse Proxy removed)
28a08e7
Raw
History Blame
8.4 kB
"""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}