tests — sandbox
tests — sandbox
The sandbox module provides a robust framework for safely executing untrusted code and commands within the system. It encompasses various levels of sandboxing, from OS-level process isolation to containerization and secure JavaScript evaluation. The primary goal is to prevent malicious or erroneous code from impacting the host system or accessing sensitive resources.
This documentation covers the key components, their responsibilities, and how they interact to provide a layered security approach.
Core Concepts
The sandbox module employs several core concepts to achieve its goals:
- Execution Policy (
ExecPolicy): A rule-based system that determines the allowed action for a given command (e.g.,allow,deny,ask,sandbox). - Sandbox Backends: Specific implementations for isolating processes, such as Docker containers or OS-level tools like Bubblewrap/Seatbelt/Landlock.
- Sandbox Registry: A centralized mechanism to register, discover, and select the most appropriate available sandbox backend based on priority and system capabilities.
- Auto-Routing: A high-level component that integrates the execution policy with available sandbox backends to automatically decide how a command should be handled.
- Safe Evaluation: A specialized sandbox for JavaScript code snippets, preventing access to sensitive Node.js globals and modules.
Architecture Overview
The following diagram illustrates the high-level interaction between the main components when a command needs to be executed:
graph TD
A[AutoSandboxRouter] -->|1. shouldSandbox(cmd)| B{ExecPolicy};
B -->|PolicyAction (allow/deny/ask/sandbox)| A;
A -->|2. route(cmd) if sandbox needed| C{SandboxRegistry};
C -->|3. getActiveSandboxBackend()| D{Available Backends};
D -->|Selects by Priority & Availability| E[OSSandbox];
D -->|Selects by Priority & Availability| F[DockerSandbox];
C -->|4. sandboxExecute(cmd)| E;
C -->|4. sandboxExecute(cmd)| F;
E -->|Executes via| G[Bubblewrap/Seatbelt/Landlock];
F -->|Executes via| H[Docker CLI];
subgraph Safe JS Evaluation
I[safeEval/safeEvalAsync/safeEvalCondition/safeInterpolate]
end
- The
AutoSandboxRouteris the entry point for deciding how to handle a command. - It consults the
ExecPolicyto determine the recommended action. - If the policy dictates sandboxing, the
AutoSandboxRouterdelegates to theSandboxRegistry. - The
SandboxRegistryidentifies the best available sandbox backend (eitherOSSandboxorDockerSandbox). - The chosen backend then executes the command using its underlying isolation mechanism.
- Separately, the
safe-evalfunctions provide a dedicated environment for secure JavaScript execution.
Key Components
ExecPolicy
The ExecPolicy class (src/sandbox/execpolicy.ts) is responsible for defining and evaluating rules that govern command execution. It acts as a gatekeeper, determining whether a command should be allowed, denied, sandboxed, or require user confirmation.
Purpose: To provide a flexible, rule-based system for enforcing security policies on external command execution.
Key Features:
- Rule-based Evaluation: Commands are matched against a set of rules, each specifying a pattern, an action (
allow,deny,ask,sandbox), and a priority. - Built-in Rules: Includes predefined rules for common safe commands (e.g.,
ls,cat), dangerous commands (e.g.,rm -rf /, fork bombs), package managers (npm,pip), and shell interpreters (bash,sh). - Dangerous Pattern Detection: Actively scans command arguments for known dangerous patterns (e.g.,
curl | bash). - Configurable Default Action: Specifies the action to take if no rule matches.
- Audit Log: Records all command evaluations for review.
- Custom Rules: Allows adding, updating, and removing custom rules dynamically.
- Singleton Pattern: Ensures a single, globally accessible instance of the policy (
getExecPolicy()).
Usage:
import { ExecPolicy, PolicyAction, getExecPolicy, initializeExecPolicy } from 39;../../src/sandbox/execpolicy39;;
class="hl-cmt">// Get the singleton instance (or initialize it)
const policy = await initializeExecPolicy();
class="hl-cmt">// Evaluate a command
const evaluation = policy.evaluate(39;npm39;, [39;install39;, 39;express39;]);
console.log(`Command 39;npm install express39; action: ${evaluation.action}`); class="hl-cmt">// 39;ask39; or 39;sandbox39;
class="hl-cmt">// Check if a command is generally allowed
if (policy.isAllowed(39;ls39;)) {
console.log(39;ls is allowed39;);
}
class="hl-cmt">// Add a custom rule
policy.addRule({
name: 39;Allow My Custom Script39;,
pattern: 39;^my-script\\.sh$39;,
isRegex: true,
action: 39;allow39;,
priority: 150,
enabled: true,
});
OSSandbox
The OSSandbox class (src/sandbox/os-sandbox.ts) provides OS-level process isolation using platform-specific tools. It aims to run commands with minimal privileges and restricted access to the filesystem and network.
Purpose: To execute commands in a lightweight, isolated environment directly on the host OS, leveraging native sandboxing capabilities.
Key Features:
- Platform Agnostic Interface: Provides a unified
execinterface regardless of the underlying OS. - Capability Detection: Automatically detects available sandboxing tools on the system:
- Linux:
bubblewrap(bwrap),Landlock(kernel feature withbwrapintegration),Seccomp(kernel feature withbwrapintegration). - macOS:
sandbox-exec(Seatbelt). - Windows: Currently falls back to
none(direct execution). - Configurable Isolation: Allows specifying a working directory, network access, read-only/read-write paths, and execution timeout.
- Landlock/Seccomp Integration: On Linux, if Landlock is available (kernel >= 5.13 or
/proc/sys/kernel/unprivileged_landlock_restrict), it attempts to usebwrapwith Landlock and a generated Seccomp BPF filter for enhanced syscall restriction. - Fallback Mechanism: If a requested backend (e.g.,
landlock) or its dependencies fail, it gracefully falls back to other available backends (e.g.,bubblewrapwithout seccomp) ornone. - Singleton Pattern: Accessible via
getOSSandbox().
Usage:
import { OSSandbox, getOSSandbox } from 39;../../src/sandbox/os-sandbox39;;
const sandbox = getOSSandbox();
class="hl-cmt">// Initialize the sandbox (detects capabilities and selects backend)
await sandbox.initialize();
if (await sandbox.isAvailable()) {
console.log(`OS Sandbox backend: ${sandbox.getBackend()}`);
class="hl-cmt">// Execute a command with network disabled and a specific work directory
const result = await sandbox.exec(39;npm39;, [39;install39;], {
workDir: 39;/tmp/my-project39;,
allowNetwork: false,
timeout: 60000,
});
if (result.success) {
console.log(39;Command output:39;, result.stdout);
} else {
console.error(39;Command failed:39;, result.stderr || result.error);
}
} else {
console.warn(39;OS Sandbox not available on this system.39;);
}
Landlock/Seccomp Specifics:
The checkLandlockSupport() function determines Landlock availability based on kernel version or /proc entry. generateSeccompFilter() creates a BPF filter that blocks specific dangerous syscalls (e.g., reboot, kexec_file_load). When OSSandbox uses the landlock backend, it writes this filter to a temporary file and passes it to bwrap using the --seccomp flag.
DockerSandbox
The DockerSandbox class (src/sandbox/docker-sandbox.ts) provides container-based isolation using Docker. This offers a higher level of isolation compared to OS-level sandboxing, as commands run within a separate container environment.
Purpose: To execute commands within isolated Docker containers, providing strong resource limits and environment control.
Key Features:
- Docker Integration: Builds and executes
docker runcommands with various isolation parameters. - Resource Limits: Configurable memory (
-m), CPU (--cpus), and network (--network none) restrictions. - Filesystem Control: Supports read-only containers (
--read-only) and mounting host directories as a workspace (-v,-w). - Execution Timeout: Automatically kills containers if execution exceeds a specified duration.
- Container Management: Tracks active containers and provides methods to
killspecific containers orpruneold ones. - Availability Check:
isAvailable()checks if the Docker daemon is running and accessible.
Usage:
import { DockerSandbox } from 39;../../src/sandbox/docker-sandbox39;;
const dockerSandbox = new DockerSandbox({
image: 39;node:22-slim39;,
memoryLimit: 39;512m39;,
cpuLimit: 39;0.539;,
networkEnabled: false,
workspaceMount: 39;/path/to/host/project39;,
timeout: 30000,
});
if (await DockerSandbox.isAvailable()) {
const result = await dockerSandbox.execute(39;npm test39;);
if (result.success) {
console.log(39;Docker command output:39;, result.output);
} else {
console.error(39;Docker command failed:39;, result.error);
}
} else {
console.warn(39;Docker is not available.39;);
}
SandboxRegistry
The SandboxRegistry (src/sandbox/sandbox-registry.ts) acts as a central hub for managing and selecting available sandbox backends. It allows different sandbox implementations (like OSSandbox and DockerSandbox) to register themselves and be chosen based on priority and availability.
Purpose:
To abstract away the complexity of choosing the "best" sandbox backend, providing a unified sandboxExecute interface.
Key Features:
- Backend Registration: Backends register themselves with a name, an implementation, and a priority.
- Priority-based Selection:
getActiveSandboxBackend()selects the highest-priority backend that reports itself asisAvailable(). - Caching: Caches the active backend to avoid repeated availability checks.
- Unified Execution:
sandboxExecute(command)uses the currently active backend to run the command. - Listing Backends:
listSandboxBackends()provides an overview of all registered backends and their availability.
Usage:
import {
registerSandboxBackend,
getActiveSandboxBackend,
sandboxExecute,
listSandboxBackends,
} from 39;../../src/sandbox/sandbox-registry39;;
import { OSSandbox } from 39;../../src/sandbox/os-sandbox39;;
import { DockerSandbox } from 39;../../src/sandbox/docker-sandbox39;;
class="hl-cmt">// Register backends (typically done during module initialization)
registerSandboxBackend(new OSSandbox(), 100); class="hl-cmt">// Higher priority
registerSandboxBackend(new DockerSandbox(), 50); class="hl-cmt">// Lower priority
class="hl-cmt">// Get the active backend
const activeBackend = await getActiveSandboxBackend();
if (activeBackend) {
console.log(`Active sandbox backend: ${activeBackend.name}`);
}
class="hl-cmt">// Execute a command using the active backend
const result = await sandboxExecute(39;echo "Hello from sandbox!"39;);
console.log(result.output);
class="hl-cmt">// List all registered backends
const backends = await listSandboxBackends();
console.log(39;Registered backends:39;, backends);
AutoSandboxRouter
The AutoSandboxRouter (src/sandbox/auto-sandbox.ts) is the top-level component that orchestrates the decision-making process for command execution. It combines the ExecPolicy with the SandboxRegistry to automatically route commands to either direct execution or a suitable sandbox.
Purpose: To provide an intelligent, automated routing mechanism for commands, applying sandboxing only when necessary and feasible.
Key Features:
- Policy Integration: Uses
ExecPolicy.evaluate()to determine if a command should be sandboxed. - Sandbox Registry Integration: If sandboxing is required, it delegates to
SandboxRegistry.sandboxExecute(). - Configurable: Can be enabled/disabled globally.
shouldSandbox(): A synchronous check to quickly determine if a command would be sandboxed based on policy.route(): The main asynchronous method that executes the command, either directly or via a sandbox, returning the execution mode (directorsandbox).
Usage:
import { AutoSandboxRouter } from 39;../../src/sandbox/auto-sandbox39;;
const router = new AutoSandboxRouter({ enabled: true });
class="hl-cmt">// Check if a command should be sandboxed
const checkResult = router.shouldSandbox(39;npm install39;);
console.log(`39;npm install39; should sandbox: ${checkResult.sandbox} (Reason: ${checkResult.reason})`);
class="hl-cmt">// Route and execute a command
const routeResult = await router.route(39;ls -la39;);
console.log(`39;ls -la39; executed in mode: ${routeResult.mode}`); class="hl-cmt">// 39;direct39;
const npmRouteResult = await router.route(39;npm install39;);
console.log(`39;npm install39; executed in mode: ${npmRouteResult.mode}`); class="hl-cmt">// 39;sandbox39; or 39;direct39; (if no backend)
class="hl-cmt">// Disable the router
router.setEnabled(false);
const disabledCheck = router.shouldSandbox(39;npm install39;);
console.log(`After disabling, 39;npm install39; should sandbox: ${disabledCheck.sandbox}`); class="hl-cmt">// false
Safe JavaScript Evaluation (safe-eval.ts)
The safe-eval.ts module provides functions for securely evaluating JavaScript code snippets within a restricted environment. This is crucial for scenarios where user-provided or untrusted JavaScript needs to be executed without granting access to the full Node.js runtime.
Purpose: To safely execute JavaScript expressions and template strings, preventing access to sensitive global objects and modules.
Key Functions:
safeEval(code, options): Evaluates a synchronous JavaScript expression.safeEvalAsync(code, options): Evaluates an asynchronous JavaScript expression (supportsawait).safeEvalCondition(code, context): Evaluates a JavaScript expression as a boolean condition, returningfalseon errors.safeInterpolate(template, context): Interpolates values from a context into a template string containing{{...}}expressions.
Key Features:
- Global Scope Restriction: Blocks access to
process,require, and other sensitive Node.js globals.globalThis.processwill beundefined. - Context Variables: Allows injecting specific variables into the evaluation scope.
- Timeout: Prevents infinite loops or long-running scripts from blocking the application.
- Error Handling: Catches syntax errors and runtime exceptions, preventing crashes.
Usage:
import { safeEval, safeEvalAsync, safeEvalCondition, safeInterpolate } from 39;../../src/sandbox/safe-eval39;;
class="hl-cmt">// Synchronous evaluation
const sum = safeEval(39;x + y39;, { context: { x: 10, y: 20 } });
console.log(39;Sum:39;, sum); class="hl-cmt">// 30
class="hl-cmt">// Asynchronous evaluation
async function runAsyncEval() {
const result = await safeEvalAsync(39;const p = Promise.resolve(100); return await p + val;39;, { context: { val: 1 } });
console.log(39;Async result:39;, result); class="hl-cmt">// 101
}
runAsyncEval();
class="hl-cmt">// Conditional evaluation
const isAllowed = safeEvalCondition(39;user.role === "admin" && item.price < 10039;, {
user: { role: 39;admin39; },
item: { price: 50 },
});
console.log(39;Is allowed:39;, isAllowed); class="hl-cmt">// true
class="hl-cmt">// Template interpolation
const message = safeInterpolate(39;Hello {{user.name}}, your total is ${{order.total.toFixed(2)}}.39;, {
user: { name: 39;Alice39; },
order: { total: 123.456 },
});
console.log(39;Message:39;, message); class="hl-cmt">// "Hello Alice, your total is $123.46."
class="hl-cmt">// Attempting unsafe operations will throw
try {
safeEval(39;process.exit(1)39;);
} catch (e) {
console.error(39;Caught unsafe eval attempt:39;, e.message);
}
Developer Notes
- Testing Sandboxes: Due to the nature of sandboxing, many tests (especially for
DockerSandboxandOSSandbox) heavily rely on mockingchild_processfunctions (execSync,spawn,spawnSync) to simulate external command execution without actually running Docker orbwrap. When contributing, ensure mocks are correctly set up and cleaned up. - Adding New Sandbox Backends: To add a new sandbox backend, implement the
SandboxBackendInterface(src/sandbox/sandbox-backend.ts) and register it with theSandboxRegistryusingregisterSandboxBackend(). Remember to consider its priority relative to existing backends. - Extending
ExecPolicy: New rules can be added toExecPolicyto handle specific commands or patterns. When adding rules, consider their priority and potential interactions with existing rules. Dangerous patterns should always have a high priority and result in adenyaction. - Platform-Specific Logic: Be mindful of platform differences when working with
OSSandbox. Capabilities and recommended backends vary significantly between Linux, macOS, and Windows. - Security Considerations: Always prioritize security. When modifying sandbox logic or adding new features, consider potential bypasses or vulnerabilities. The goal is to minimize the attack surface for untrusted code.