Sandbox Security: Control What Untrusted Code Can Reach

Peter Bubenik · Vercel · · Source
Image for A sandbox without a network boundary is only half a sandbox

Step-by-Step Study Material

Step 1: Understand What a Sandbox Is Trying to Do

Before adding complexity, establish the foundation.

A sandbox is an isolated execution environment for running untrusted code. The goal is to contain the code's consequences — not just the code itself.

Two core questions a sandbox must answer:

QuestionAnswered By
What can this code access on this machine?Compute isolation (microVM, container)
What can this code access through the network?Network isolation (egress control)

Key insight: Most people think of sandboxing as only the first question. This is the central mistake the article addresses.


Step 2: Understand Why Compute Isolation Alone Fails

What compute isolation does well:

  • Prevents code from reading host files
  • Prevents code from accessing other workloads on the same machine
  • Isolates the kernel and process space

What compute isolation cannot prevent:

Untrusted code inside microVM
        │
        │  (VM boundary intact, no escape needed)
        │
        ▼
Outbound network connection ──────► External attacker's server
        │
        ├── Exfiltrate private files
        ├── Send environment variables / credentials
        ├── Scan internal network services
        └── Call authenticated APIs with stolen tokens

Concrete example to remember:

An AI agent reads a code repository. A prompt injection hidden in a comment instructs it to upload all files to evil.com. The microVM boundary is never crossed. The code simply opens a network connection and sends the data.

The VM worked perfectly. Containment still failed.


Step 3: Recognize Network Bypass Patterns

These are real attack vectors — not theoretical. Learn to identify each one.

Pattern 1: DNS Left Open

  • Environment appears disconnected
  • But DNS resolver is still reachable
  • DNS queries can encode and exfiltrate data (DNS tunneling)

Pattern 2: Allowlist That Fails Open

  • Empty or misconfigured allowlist defaults to allow instead of deny
  • All traffic passes through

Pattern 3: Hostname Interpretation Mismatch

  • Policy engine reads hostname one way
  • Proxy resolves it differently
  • Attacker crafts a hostname that passes the policy but reaches a different destination

Pattern 4: Trusted Service Used as Relay

  • A permitted package registry or CDN is abused
  • Untrusted code uploads data to it, attacker retrieves it
  • The allowed destination becomes an exfiltration channel

The critical principle:

A network bypass is a sandbox escape — even if the VM boundary never breaks.


Step 4: Learn the Principle of Selective Connectivity

Complete network disconnection is safe but often impractical. A real workload may need to:

  • Clone a repository
  • Install dependencies
  • Call an AI model API
  • Upload results to storage

The solution is selective connectivity — grant only what the workload actually needs.

Spectrum of network policies:

FULLY OPEN ◄────────────────────────────► FULLY ISOLATED
     │                                           │
     │         USEFUL SANDBOX LIVES HERE         │
     │                                           │
  Dangerous                              Often impractical

Examples of granular policies:

NeedPolicy
Use one AI providerAllow ai-provider.com, deny everything else
Write to storageAllow one specific S3 bucket, not entire AWS
Access internal serviceAllow one private IP, block rest of private range
Install packages safelyAllow registry during setup, remove access before untrusted code runs

Two complementary policy tools:

Domain rules — for modern cloud services

  • Services whose IPs change frequently
  • Multiple services sharing IPs (CDNs, SaaS)
  • Example: allow: "api.openai.com"

CIDR rules — for infrastructure

  • Fixed IP ranges
  • Private networks (10.0.0.0/8, 192.168.0.0/16)
  • Protocol-agnostic control

Use both together. They solve different problems.


Step 5: Understand Dynamic (Time-Based) Policies

Network permissions should change throughout a workload's lifecycle, not remain static.

Example lifecycle:

Phase 1: SETUP
├── Allow: package registry (npm, pip)
├── Allow: git repository
└── Allow: AI model API

        ▼  [Untrusted code is about to run]

Phase 2: EXECUTION  ← tighten here
├── Deny: package registry
├── Deny: git repository
└── Allow: AI model API only

        ▼  [Code finished, need to save results]

Phase 3: OUTPUT
├── Allow: one specific storage bucket
└── Deny: everything else

        ▼  [Done]

Phase 4: COMPLETE
└── Deny: all outbound traffic

This is achievable without restarting the workload — policies update at runtime.


Step 6: Understand Where the Firewall Must Live

This is an architectural requirement, not just a preference.

Wrong placement:

[microVM]
  └── Firewall running inside VM
        └── Untrusted code can modify or disable it

Correct placement:

[Host machine]
  └── Firewall running on host  ← untrusted code cannot reach this
        └── [microVM]
              └── Untrusted code

The firewall must run outside the sandbox it governs.

How it works technically (Vercel's implementation):

  1. Linux networking transparently redirects all outbound TCP and DNS through the host firewall
  2. Workloads need no proxy configuration — they behave normally
  3. For TLS connections, the firewall reads the SNI (Server Name Indication) — the unencrypted part of the TLS handshake that identifies the destination hostname
  4. It checks SNI against domain policy, checks destination IP against CIDR policy
  5. Allowed connections pass through without decryption

SNI explained simply: When your browser starts a TLS connection, it announces the hostname it wants in plaintext before encryption begins. This is so servers hosting multiple domains know which certificate to use. The firewall reads only this announcement — it does not decrypt your traffic.


Step 7: Understand Credential Injection

This solves a specific and important problem.

The problem with credentials inside a sandbox:

Environment variable: API_KEY=sk-abc123
        │
        ├── Legitimate code reads it ✓
        ├── Malicious code reads it ✓
        └── Malicious code sends it to attacker's server ✓
              └── Attacker now has your API key forever
                  even after sandbox is destroyed

The solution — inject credentials at the network boundary:

[Host Firewall]
  ├── Holds the real credential (never enters VM)
  ├── Creates a unique certificate authority for this sandbox
  └── When sandbox connects to allowed destination:
        1. Terminates TLS from sandbox
        2. Reads the request
        3. Injects Authorization header with real credential
        4. Opens new TLS connection to real destination
        5── Credential traveled only on the host, never inside VM

Benefits of this approach:

PropertyResult
Credential never in VMCannot be read by any code inside sandbox
Credential never leaves host unencryptedCannot be intercepted
CA is unique per sandboxDisposed when sandbox stops
Credential only sent to configured destinationUploading files elsewhere doesn't transfer the credential
Can restrict by path/methodCode can POST results but not GET other resources

Step 8: Understand Programmable Policy (Request Forwarding)

Static allowlists cannot cover every requirement. Sometimes you need custom logic in the security path.

What request forwarding enables:

Selected HTTPS requests are forwarded to your own proxy before reaching their destination.

Your proxy receives:

  • The original request
  • A Vercel OIDC token identifying which team/project/sandbox sent it

What your proxy can do:

Incoming request from sandbox
        │
        ▼
[Your Proxy — outside sandbox]
        │
        ├── Redact sensitive fields from request body
        ├── Check per-user authorization rules
        ├── Scan packages for supply chain attacks
        ├── Log request for compliance audit trail
        ├── Exchange sandbox identity for scoped credential
        └── Reject operations that violate business rules
        │
        ▼
Destination service (if approved)

Key point: Your policy logic and secrets stay outside the sandbox, even when the code inside has full root access within its microVM.


Step 9: Apply It — Read the Code Example

Now the implementation makes sense:

import { Sandbox } from '@vercel/sandbox';

const sandbox = await Sandbox.create({
  networkPolicy: {
    allow: {
      // Only this domain is permitted
      "ai-gateway.vercel.sh": [{
        transform: [{
          headers: {
            // Credential injected at network boundary
            // Never stored inside the sandbox
            "Authorization": `Bearer ${process.env.AI_GATEWAY_TOKEN}`
          }
        }]
      }]
    }
    // All other destinations: denied by default
  }
});

What this policy enforces:

  • ✅ Sandbox can reach ai-gateway.vercel.sh
  • ✅ Requests automatically receive the auth header
  • ✅ The token value never enters the microVM
  • ❌ All other outbound connections are blocked

Updating policy at runtime (dynamic lifecycle):

// When untrusted phase begins, lock it down completely
await sandbox.update({ networkPolicy: 'deny-all' });

Step 10: Synthesize — The Complete Mental Model

Bring everything together into one coherent picture:

COMPLETE SANDBOX = Compute Isolation + Network Isolation

Compute Isolation answers:
  "What can code access on this machine?"

Network Isolation answers:
  "What can code access through the network?"
  "What credentials can it use?"
  "Which requests need additional policy?"
  "When does all communication stop?"

The five questions every sandbox design must answer:

  1. Which destinations can this sandbox reach?
  2. Which address ranges are blocked (especially private networks)?
  3. Which requests can carry credentials — and which credentials?
  4. Which operations must pass through additional policy logic?
  5. When should all communication stop?

The fundamental principle to remember:

A sandbox is not defined only by where its code runs. It is defined by what that code can reach, what authority it receives, and which boundaries hold when the code itself is hostile.


Quick Reference Summary

ConceptOne-Line Summary
Compute isolationPrevents access to the host machine
Network isolationPrevents access through the network
Egress controlAllowlist outbound destinations; deny by default
Domain policiesControl connections by hostname
CIDR policiesControl connections by IP range
Dynamic policiesChange permissions during workload lifecycle
Firewall placementMust be on host, outside the VM
SNI inspectionRead hostname from unencrypted TLS handshake
Credential injectionAdd auth headers at network boundary, not inside VM
Request forwardingRoute selected requests through your own policy proxy
Network bypass = escapeA bypass achieves the same harm as a VM escape

More to study