Security & Sandbox
Run untrusted code safely with cross-origin isolation, Web Worker execution, and sandboxed iframes.
Security Modes
almostnode provides three execution modes with different security levels. Use createRuntime() to select a mode:
| Mode | Option | Security | Use Case |
|---|---|---|---|
| Cross-origin sandbox | sandbox: 'https://...' |
Highest | Production, untrusted code |
| Same-origin Worker | dangerouslyAllowSameOrigin: true, useWorker: true |
Medium | Demos with trusted code |
| Same-origin main thread | dangerouslyAllowSameOrigin: true |
Lowest | Trusted code only |
createRuntime() throws an error if neither sandbox nor dangerouslyAllowSameOrigin is provided. You must explicitly choose a security level.
Cross-Origin Sandbox (Recommended)
The most secure option. Code runs in a cross-origin iframe, fully isolated from your main application:
import { createRuntime, VirtualFS } from 'almostnode';
const vfs = new VirtualFS();
const runtime = await createRuntime(vfs, {
sandbox: 'https://your-sandbox.vercel.app',
});
// Safe to run untrusted code
const result = await runtime.execute(untrustedCode);
Same-Origin with Worker
Runs code in a Web Worker on the same origin. Offloads execution from the main thread but shares the origin with your app:
const runtime = await createRuntime(vfs, {
dangerouslyAllowSameOrigin: true,
useWorker: true,
});
Same-Origin Main Thread
Runs code directly on the main thread. Simplest setup but provides no isolation — only use with fully trusted code:
const runtime = await createRuntime(vfs, {
dangerouslyAllowSameOrigin: true,
});
The createContainer() helper always uses same-origin main thread execution. For secure sandboxing, use createRuntime() directly.
REPL & eval() Security
The runtime.createREPL() method uses eval() internally to return expression values. While the eval runs in an isolated generator closure (not the global scope), it shares the same security boundary as execute():
| Execution Context | eval() Safety | Risk |
|---|---|---|
| Cross-origin sandbox iframe | Safe | Code cannot access host page cookies, localStorage, or DOM |
| Same-origin Worker | Moderate | Code can access IndexedDB and make network requests, but not the DOM |
| Same-origin main thread | Dangerous for untrusted input | Code can access cookies, localStorage, DOM, and all page APIs |
If you let users type arbitrary code into a REPL on the main thread or in a same-origin iframe, that code can access everything your page can: cookies, auth tokens, localStorage, the DOM, and network APIs. For any user-facing REPL or code editor, always use a cross-origin sandboxed iframe.
Sandbox Setup
The cross-origin sandbox must be deployed at a different origin (different domain, subdomain, or port) from your main application.
Quick Setup (Vercel)
Use generateSandboxFiles() to create all required files, then deploy to Vercel:
import { generateSandboxFiles } from 'almostnode';
import fs from 'fs';
const files = generateSandboxFiles();
fs.mkdirSync('sandbox', { recursive: true });
for (const [filename, content] of Object.entries(files)) {
fs.writeFileSync(`sandbox/${filename}`, content);
}
// Deploy: cd sandbox && vercel --prod
Generated Files
| File | Purpose |
|---|---|
index.html | Sandbox page that loads almostnode and registers the service worker |
vercel.json | CORS headers for cross-origin iframe embedding |
__sw__.js | Service worker for intercepting dev server requests |
Manual Setup (Any Platform)
If you're not using Vercel, you need two things: a sandbox HTML page and the correct CORS headers.
1. Sandbox HTML Page
Create an index.html that loads almostnode and handles postMessage communication with your main app:
<script type="module">
import { VirtualFS, Runtime } from 'https://unpkg.com/almostnode/dist/index.js';
let vfs = null;
let runtime = null;
window.addEventListener('message', async (event) => {
const { type, id, code, vfsSnapshot, options } = event.data;
switch (type) {
case 'init':
vfs = VirtualFS.fromSnapshot(vfsSnapshot);
runtime = new Runtime(vfs, options);
break;
case 'execute':
const result = runtime.execute(code);
parent.postMessage({ type: 'result', id, result }, '*');
break;
}
});
parent.postMessage({ type: 'ready' }, '*');
</script>
2. CORS Headers
The sandbox server must include these headers to allow cross-origin iframe embedding:
Access-Control-Allow-Origin: *
Cross-Origin-Resource-Policy: cross-origin
Example server configurations:
Nginx:
server {
listen 3002;
root /path/to/sandbox;
location / {
add_header Access-Control-Allow-Origin *;
add_header Cross-Origin-Resource-Policy cross-origin;
}
}
Express.js:
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
next();
});
app.use(express.static('sandbox'));
What the Sandbox Protects
| Threat | Status |
|---|---|
| Cookies | Blocked (different origin) |
| localStorage | Blocked (different origin) |
| IndexedDB | Blocked (different origin) |
| DOM access | Blocked (cross-origin iframe) |
Network requests from the sandbox are still possible. Add Content Security Policy headers for additional protection.
Local Development
For local testing, run the sandbox on a different port:
# Terminal 1: Main app on port 5173
npm run dev
# Terminal 2: Sandbox on port 3002
npm run sandbox
Then use sandbox: 'http://localhost:3002/sandbox/' in your app.
Service Worker Setup
almostnode uses a service worker to intercept HTTP requests and route them to virtual dev servers. The service worker is required when using ServerBridge with dev servers.
The service worker is only needed if you're using dev servers with URL access (e.g., /__virtual__/3000/). If you're only executing code with runtime.execute(), you don't need it.
| Use Case | Setup |
|---|---|
| Cross-origin sandbox | generateSandboxFiles() — includes everything |
| Same-origin with Vite | almostnodePlugin from almostnode/vite |
| Same-origin with Next.js | getServiceWorkerContent from almostnode/next |
| Other frameworks | Manual copy to public directory |
Vite Plugin
For same-origin setups with Vite, use the built-in plugin:
// vite.config.ts
import { defineConfig } from 'vite';
import { almostnodePlugin } from 'almostnode/vite';
export default defineConfig({
plugins: [almostnodePlugin()]
});
The plugin automatically serves /__sw__.js during development. You can customize the path:
almostnodePlugin({ swPath: '/custom/__sw__.js' })
// Then in your app:
await bridge.initServiceWorker({ swUrl: '/custom/__sw__.js' });
Next.js Route Handler
For same-origin setups with Next.js:
App Router
// app/__sw__.js/route.ts
import { getServiceWorkerContent } from 'almostnode/next';
export async function GET() {
return new Response(getServiceWorkerContent(), {
headers: {
'Content-Type': 'application/javascript',
'Cache-Control': 'no-cache',
},
});
}
Pages Router
// pages/api/__sw__.ts
import { getServiceWorkerContent } from 'almostnode/next';
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Cache-Control', 'no-cache');
res.send(getServiceWorkerContent());
}
Manual Setup
For other frameworks, copy the service worker to your public directory:
cp node_modules/almostnode/dist/__sw__.js ./public/
Or programmatically:
import { getServiceWorkerPath } from 'almostnode/next';
import fs from 'fs';
fs.copyFileSync(getServiceWorkerPath(), './public/__sw__.js');
HMR with Sandboxed Iframes
Hot module replacement works with sandboxed iframes using postMessage for communication between your main page and the preview iframe.
Iframe Setup
// Create sandboxed iframe for security
const iframe = document.createElement('iframe');
iframe.src = '/__virtual__/3000/';
iframe.sandbox = 'allow-forms allow-scripts allow-same-origin allow-popups';
container.appendChild(iframe);
// Register as HMR target after load
iframe.onload = () => {
if (iframe.contentWindow) {
devServer.setHMRTarget(iframe.contentWindow);
}
};
Sandbox Permissions
allow-scripts— required for JavaScript executionallow-same-origin— allows cookies, localStorage, IndexedDB access (omit for better isolation)allow-forms— if your app uses formsallow-popups— if your app opens new windows/tabs
Manual HMR Triggering
If you need to manually trigger HMR updates after programmatic file changes:
function triggerHMR(path: string, iframe: HTMLIFrameElement) {
if (iframe.contentWindow) {
iframe.contentWindow.postMessage({
type: 'update',
path,
timestamp: Date.now(),
channel: 'next-hmr', // Use 'vite-hmr' for Vite
}, '*');
}
}
// After writing a file
vfs.writeFileSync('/app/page.tsx', newContent);
triggerHMR('/app/page.tsx', iframe);
Supported File Types
| File Type | HMR Behavior |
|---|---|
.jsx, .tsx | React Refresh (preserves state) |
.js, .ts | Full module reload |
.css | Style injection (no reload) |
.json | Full page reload |