Overview
Cloudflare Sandbox provides isolated execution environments, but proper security requires understanding isolation boundaries, implementing access controls, and following secure coding practices.
Isolation model
Container-level isolation
Each sandbox runs in a separate container with:
- Process isolation - Separate process namespace
- Filesystem isolation - Isolated root filesystem
- Network isolation - Separate network stack
- Resource limits - CPU, memory, and disk quotas
Isolation is provided by Cloudflare’s platform, not by the SDK. Containers run in VMs with hardware-level isolation.
What’s shared between sandboxes
Nothing is shared by default. Each sandbox is completely isolated:
const sandbox1 = getSandbox(env.SANDBOX, 'user-1');
const sandbox2 = getSandbox(env.SANDBOX, 'user-2');
// These are completely isolated - no shared state
await sandbox1.exec('echo "secret" > /tmp/data.txt');
await sandbox2.exec('cat /tmp/data.txt'); // File not found
Session isolation
Sessions within the same sandbox share the filesystem but have separate:
- Working directories (can differ)
- Environment variables (per session)
- Shell state (independent shells)
const session1 = await sandbox.createSession({
id: 'session-1',
cwd: '/workspace/project-a'
});
const session2 = await sandbox.createSession({
id: 'session-2',
cwd: '/workspace/project-b'
});
// Filesystem is shared
await session1.exec('echo "data" > /shared/file.txt');
await session2.exec('cat /shared/file.txt'); // "data"
// Environment variables are isolated
await session1.exec('export API_KEY=secret1');
const result = await session2.exec('echo $API_KEY');
console.log(result.stdout); // Empty - not inherited
Access control
Sandbox ID security
Sandbox IDs act as access tokens. Keep them unpredictable:
// Bad: Predictable IDs enable unauthorized access
const sandbox = getSandbox(env.SANDBOX, 'user-123');
// Good: Cryptographically random IDs
import { randomUUID } from 'crypto';
const sandboxId = `${userId}-${randomUUID()}`;
const sandbox = getSandbox(env.SANDBOX, sandboxId);
Store sandbox IDs securely:
// Store in Durable Object storage
await this.ctx.storage.put(`user:${userId}:sandbox`, sandboxId);
// Or in KV with encryption
const encrypted = await encrypt(sandboxId, env.ENCRYPTION_KEY);
await env.KV.put(`sandbox:${userId}`, encrypted);
Validate user access
interface UserSession {
userId: string;
sandboxId: string;
}
export default {
async fetch(request: Request, env: Env) {
// Verify user owns the sandbox
const session = await getUserSession(request);
const allowedSandboxId = await env.KV.get(`user:${session.userId}:sandbox`);
const requestedSandboxId = new URL(request.url).searchParams.get('sandbox');
if (requestedSandboxId !== allowedSandboxId) {
return new Response('Forbidden', { status: 403 });
}
const sandbox = getSandbox(env.SANDBOX, allowedSandboxId);
// Proceed with authorized access
}
};
Sanitize sandbox IDs
The SDK enforces DNS-compliant sandbox IDs:
try {
const sandbox = getSandbox(env.SANDBOX, 'user-@#$%');
} catch (error) {
// SecurityError: Invalid sandbox ID
}
Rules enforced:
- 1-63 characters
- Cannot start/end with hyphens
- Reserved names blocked (www, api, admin, etc.)
Escape shell commands
Never pass unsanitized user input to shell commands:
import { shellEscape } from '@repo/shared';
// Bad: Command injection vulnerability
const userInput = "test; rm -rf /";
await sandbox.exec(`echo ${userInput}`);
// Good: Proper escaping
const escaped = shellEscape(userInput);
await sandbox.exec(`echo ${escaped}`);
// Better: Use parameterized operations
await sandbox.writeFile('/output.txt', userInput);
Validate file paths
function isValidPath(path: string): boolean {
// Must be absolute
if (!path.startsWith('/')) return false;
// No directory traversal
if (path.includes('..')) return false;
// Limit to allowed directories
const allowed = ['/workspace', '/tmp', '/home'];
if (!allowed.some(dir => path.startsWith(dir))) return false;
return true;
}
const userPath = request.headers.get('X-File-Path');
if (!isValidPath(userPath)) {
return new Response('Invalid path', { status: 400 });
}
await sandbox.readFile(userPath);
Validate port numbers
The SDK validates ports, but enforce application-level rules:
const ALLOWED_PORTS = [8080, 8081, 8082];
const port = Number.parseInt(request.headers.get('X-Port') || '0');
if (!ALLOWED_PORTS.includes(port)) {
return new Response('Port not allowed', { status: 403 });
}
await sandbox.exposePort(port, `service-${port}`);
Credential management
Never hardcode secrets
// Bad: Hardcoded credentials
await sandbox.mountBucket('my-bucket', '/mnt/data', {
endpoint: 'https://account-id.r2.cloudflarestorage.com',
credentials: {
accessKeyId: 'AKIAIOSFODNN7EXAMPLE', // DON'T DO THIS!
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
}
});
// Good: Use environment bindings
await sandbox.mountBucket('my-bucket', '/mnt/data', {
endpoint: env.R2_ENDPOINT,
credentials: {
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY
}
});
Inject secrets securely
// Set environment variables in sandbox
await sandbox.exec(`export API_KEY=${shellEscape(env.API_KEY)}`);
// Or write to secure file
await sandbox.writeFile('/.env', `API_KEY=${env.API_KEY}`, {
mode: 0o600 // Read/write for owner only
});
// Use in commands
await sandbox.exec('source /.env && run-app');
Rotate credentials
class CredentialRotation {
private currentCreds: Credentials;
private nextRotation: number;
async getCredentials(env: Env): Promise<Credentials> {
if (Date.now() > this.nextRotation) {
// Fetch new credentials from secrets manager
this.currentCreds = await env.SECRETS.get('db-credentials');
this.nextRotation = Date.now() + 3600000; // 1 hour
}
return this.currentCreds;
}
}
Network security
Restrict outbound access
Containers have full internet access by default. Implement application-level controls:
// Allowlist for outbound connections
const ALLOWED_DOMAINS = [
'api.example.com',
'cdn.example.com'
];
async function fetchSecurely(url: string) {
const hostname = new URL(url).hostname;
if (!ALLOWED_DOMAINS.includes(hostname)) {
throw new Error('Domain not allowed');
}
return fetch(url);
}
Secure port exposure
Exposed ports are publicly accessible. Use authentication:
// Generate secure token for port access
import { randomBytes } from 'crypto';
const token = randomBytes(32).toString('hex');
// Store token securely
await env.KV.put(`port:${port}:token`, token, {
expirationTtl: 3600 // 1 hour
});
// Expose port
const url = await sandbox.exposePort(8080, 'api');
// Application must verify token
// (implement in your app running on port 8080)
Production requirement:
Preview URLs require a custom domain with wildcard DNS (*.yourdomain.com). The .workers.dev domain does NOT support preview URL subdomain patterns.
Code execution security
Untrusted code isolation
When executing user-provided code:
// Create isolated context for user code
const userContext = await sandbox.createContext({
packages: [] // Minimal dependencies
});
// Execute with timeout
try {
const result = await sandbox.runCode(userCode, {
language: 'python',
contextId: userContext.id,
timeout: 30000 // 30 seconds
});
} catch (error) {
// Handle timeout or execution errors
} finally {
// Cleanup
await sandbox.deleteContext(userContext.id);
}
Resource limits
// Limit memory usage
await sandbox.exec('ulimit -v 1000000 && python user_script.py');
// Limit CPU time
await sandbox.exec('timeout 30s python user_script.py');
// Limit file size
await sandbox.exec('ulimit -f 10000 && python user_script.py');
Prevent infinite loops
// Use command timeout
await sandbox.exec('potentially-infinite-loop', {
timeout: 60000 // Kill after 1 minute
});
// Or process timeout for background execution
const process = await sandbox.startProcess({
command: 'long-running-task',
waitUntilReady: {
timeout: 30000 // Must become ready within 30s
}
});
Data security
Encrypt sensitive data
import { subtle } from 'crypto';
async function encryptData(data: string, key: CryptoKey) {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const encrypted = await subtle.encrypt(
{ name: 'AES-GCM', iv: crypto.getRandomValues(new Uint8Array(12)) },
key,
dataBuffer
);
return Buffer.from(encrypted).toString('base64');
}
// Store encrypted data in sandbox
const encrypted = await encryptData(sensitiveData, env.ENCRYPTION_KEY);
await sandbox.writeFile('/secure/data.enc', encrypted);
Clean up sensitive data
try {
// Write temporary credentials
await sandbox.writeFile('/tmp/creds.json', JSON.stringify(creds));
// Use credentials
await sandbox.exec('app --credentials /tmp/creds.json');
} finally {
// Always cleanup
await sandbox.exec('shred -u /tmp/creds.json');
}
Secure file permissions
// Write with restricted permissions
await sandbox.exec(`cat > /secure/key.pem << 'EOF'
${privateKey}
EOF`);
await sandbox.exec('chmod 600 /secure/key.pem');
// Verify permissions
const result = await sandbox.exec('ls -la /secure/key.pem');
console.log('Permissions:', result.stdout); // -rw------- (owner only)
Audit and monitoring
Log security events
interface SecurityEvent {
timestamp: string;
userId: string;
sandboxId: string;
action: string;
success: boolean;
details?: any;
}
class SecurityLogger {
async log(event: SecurityEvent, env: Env) {
// Store in analytics
await env.ANALYTICS.writeDataPoint({
indexes: [event.userId, event.sandboxId],
blobs: [event.action],
doubles: [event.success ? 1 : 0]
});
// Alert on suspicious activity
if (!event.success) {
await this.alert(event);
}
}
private async alert(event: SecurityEvent) {
// Send to monitoring service
}
}
Monitor suspicious activity
class ThreatDetector {
private failedAttempts = new Map<string, number>();
async checkAccess(userId: string, sandboxId: string) {
const key = `${userId}:${sandboxId}`;
const attempts = this.failedAttempts.get(key) || 0;
if (attempts >= 5) {
// Rate limit after 5 failed attempts
throw new Error('Too many failed attempts');
}
// Verify access
const allowed = await this.verifyAccess(userId, sandboxId);
if (!allowed) {
this.failedAttempts.set(key, attempts + 1);
throw new Error('Unauthorized');
}
// Clear failed attempts on success
this.failedAttempts.delete(key);
}
private async verifyAccess(userId: string, sandboxId: string): Promise<boolean> {
// Implement access verification
return true;
}
}
Track resource usage
interface ResourceMetrics {
sandboxId: string;
cpuTime: number;
memoryUsage: number;
diskUsage: number;
networkEgress: number;
}
class ResourceMonitor {
async collect(sandbox: Sandbox): Promise<ResourceMetrics> {
const cpu = await sandbox.exec('ps aux | awk \'{sum+=$3} END {print sum}\'');
const memory = await sandbox.exec('free -m | awk \'/Mem/ {print $3}\'');
const disk = await sandbox.exec('df -h / | awk \'/\\// {print $3}\'');
return {
sandboxId: 'sandbox-id',
cpuTime: Number.parseFloat(cpu.stdout),
memoryUsage: Number.parseInt(memory.stdout),
diskUsage: Number.parseInt(disk.stdout),
networkEgress: 0 // Track separately
};
}
async checkLimits(metrics: ResourceMetrics) {
if (metrics.memoryUsage > 1000) { // 1GB
throw new Error('Memory limit exceeded');
}
if (metrics.diskUsage > 10000) { // 10GB
throw new Error('Disk limit exceeded');
}
}
}
Common vulnerabilities
Command injection
// Vulnerable
const filename = req.query.file; // "test.txt; rm -rf /"
await sandbox.exec(`cat ${filename}`);
// Fixed
import { shellEscape } from '@repo/shared';
const escaped = shellEscape(filename);
await sandbox.exec(`cat ${escaped}`);
// Better
await sandbox.readFile(filename);
Path traversal
// Vulnerable
const path = req.query.path; // "../../etc/passwd"
await sandbox.readFile(`/workspace/${path}`);
// Fixed
function sanitizePath(userPath: string): string {
// Remove leading slashes and resolve ..
const clean = userPath.replace(/^\/+/, '').replace(/\.\./g, '');
return `/workspace/${clean}`;
}
const safe = sanitizePath(path);
await sandbox.readFile(safe);
Credential leakage
// Vulnerable - logged to stdout
await sandbox.exec(`echo ${env.API_KEY}`);
// Fixed - use environment variable
await sandbox.exec('export API_KEY=${shellEscape(env.API_KEY)}');
await sandbox.exec('my-app'); // App reads from environment
Denial of service
// Vulnerable - no timeout
await sandbox.exec(':(){ :|:& };:'); // Fork bomb
// Fixed - timeout and resource limits
await sandbox.exec('user-command', {
timeout: 30000 // 30 second timeout
});
await sandbox.exec('ulimit -u 100 && user-command'); // Limit processes
Security checklist
Before deploying to production:
Reporting security issues
If you discover a security vulnerability:
- Do not open a public GitHub issue
- Email security@cloudflare.com with details
- Include reproduction steps if possible
- Allow time for response and patching
For general security questions, refer to the Cloudflare Security documentation.