Skip to content

feat: Allow sandbox-SDK user-land control over HTTP requests from the sandbox#117

Open
cramforce wants to merge 1 commit intomainfrom
malte/js-http-control
Open

feat: Allow sandbox-SDK user-land control over HTTP requests from the sandbox#117
cramforce wants to merge 1 commit intomainfrom
malte/js-http-control

Conversation

@cramforce
Copy link
Copy Markdown
Contributor

@cramforce cramforce commented Mar 27, 2026

Summary

Add two new packages that allow JS callers to intercept and control all outbound HTTP and HTTPS requests from inside a Vercel Sandbox. A Go-based HTTP proxy runs inside the sandbox VM, tunneling requests over WebSocket to TypeScript callbacks where they can be inspected, modified, or blocked.

  • @vercel/http-proxy-server — Go binary (HTTP proxy + WS server + TLS MITM)
  • @vercel/http-proxy-tunnel — TypeScript client (WsProxy class)

Usage

Basic: intercept all requests (HTTP and HTTPS)

import { Sandbox } from "@vercel/sandbox";
import { createWsProxy } from "@vercel/http-proxy-tunnel";

const sandbox = await Sandbox.create({ ports: [5000] });
const proxy = createWsProxy();
await proxy.attach(sandbox, { wsPort: 5000 });

const handle = await proxy.handle((request) => {
  // Works for both http:// and https:// URLs
  console.log(`${request.method} ${request.url}`);
  return new Response("blocked", { status: 403 });
});

await sandbox.runCommand({
  cmd: "curl",
  args: ["-s", "https://example.com"],
  env: { ...handle.env },
});

HTTPS interception with real fetching

The proxy does TLS MITM — it generates a CA cert at startup, installs it in the sandbox trust store, and terminates TLS on CONNECT. The JS handler sees the full decrypted URL and can fetch on behalf of the sandbox:

const sandbox = await Sandbox.create({
  ports: [5000],
  networkPolicy: "deny-all", // no direct internet access
});
const proxy = createWsProxy();
await proxy.attach(sandbox, { wsPort: 5000 });

const handle = await proxy.handle(async (req) => {
  // req.url is "https://vercel.com/robots.txt" — fully decrypted
  // The handler runs OUTSIDE the sandbox, so it CAN fetch
  return fetch(req.url);
});

const result = await sandbox.runCommand({
  cmd: "curl",
  args: ["-s", "https://vercel.com/robots.txt"],
  env: { ...handle.env },
});
// result.stdout() contains the actual robots.txt content

Per-command security policies

const allowList = await proxy.handle(async (req) => {
  const url = new URL(req.url);
  if (url.hostname === "registry.npmjs.org") {
    return fetch(req.url);
  }
  return new Response("Forbidden", { status: 403 });
});

const allowAll = await proxy.handle((req) => fetch(req.url));

// Untrusted code can only reach npm
await sandbox.runCommand({
  cmd: "node", args: ["untrusted.js"],
  env: { ...allowList.env },
});

// Trusted code has full access
await sandbox.runCommand({
  cmd: "node", args: ["trusted.js"],
  env: { ...allowAll.env },
});

HTTPS CONNECT allow/deny

The optional connectHandler controls whether HTTPS connections are allowed before MITM begins:

const handle = await proxy.handle(
  async (req) => fetch(req.url),
  (host) => host.endsWith(".github.com"), // only MITM GitHub
);

Multiple independent clients sharing one sandbox

const proxyA = createWsProxy();
await proxyA.attach(sandbox, { wsPort: 5000 }); // starts server

const proxyB = createWsProxy();
await proxyB.attach(sandbox, { wsPort: 5000 }); // reuses existing server

const handleA = await proxyA.handle(() => new Response("from A"));
const handleB = await proxyB.handle(() => new Response("from B"));

const [a, b] = await Promise.all([
  sandbox.runCommand({ cmd: "curl", args: ["-s", "http://x.com"],
    env: { ...handleA.env } }),
  sandbox.runCommand({ cmd: "curl", args: ["-s", "http://x.com"],
    env: { ...handleB.env } }),
]);
// a.stdout() === "from A", b.stdout() === "from B"

ProxyHandle object

proxy.handle() returns a ProxyHandle with:

  • handle.url — the raw proxy URL string
  • handle.env — ready-made env record (HTTP_PROXY, http_proxy, HTTPS_PROXY, https_proxy)
  • handle.toString() — returns URL for string coercion

Architecture

  • Session routing via HTTP proxy auth (http://<sessionId>:x@localhost:<port>)
  • JSON protocol over WebSocket (request/response with base64 bodies)
  • HTTPS MITM: in-memory CA + per-hostname leaf certs, auto-installed in sandbox trust store
  • Multi-client: Go hub tracks session-to-client mappings via register/unregister with ack
  • Server reuse: config persisted so multiple clients share one proxy

Test plan

  • 25 Go unit tests (protocol, hub, proxy server, MITM, session extraction)
  • 8 TypeScript process tests (spawn binary, WS client, HTTP/HTTPS proxy)
  • 8 sandbox integration tests (real Sandbox, deny-all + HTTPS fetch, multi-client)

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sandbox Error Error Open in v0 Mar 27, 2026 9:00pm
sandbox-cli Ready Ready Preview, Comment, Open in v0 Mar 27, 2026 9:00pm
sandbox-sdk Ready Ready Preview, Comment, Open in v0 Mar 27, 2026 9:00pm
sandbox-sdk-ai-example Ready Ready Preview, Comment, Open in v0 Mar 27, 2026 9:00pm
workflow-code-runner Error Error Open in v0 Mar 27, 2026 9:00pm

Request Review

@socket-security
Copy link
Copy Markdown

socket-security bot commented Mar 27, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgolang/​github.com/​google/​uuid@​v1.6.0100100100100100

View full report

"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"lib": ["ESNext"],
Copy link
Copy Markdown

@vercel vercel bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing "DOM" in tsconfig.json lib array causes typecheck failure: error TS2304: Cannot find name 'BodyInit'

Fix on Vercel

Add two new packages that allow JS callers to intercept and control all
outbound HTTP and HTTPS requests from inside a Vercel Sandbox. A Go-based
HTTP proxy runs inside the sandbox VM, tunneling requests over WebSocket
to TypeScript callbacks where they can be inspected, modified, or blocked.

## Packages

- `@vercel/http-proxy-server` — Go binary (HTTP proxy + WS server + TLS MITM)
- `@vercel/http-proxy-tunnel` — TypeScript client (`WsProxy` class)

## Usage

### Basic: intercept all requests (HTTP and HTTPS)

```ts
import { Sandbox } from "@vercel/sandbox";
import { createWsProxy } from "@vercel/http-proxy-tunnel";

const sandbox = await Sandbox.create({ ports: [5000] });
const proxy = createWsProxy();
await proxy.attach(sandbox, { wsPort: 5000 });

const httpProxy = proxy.handle((request) => {
  // Works for both http:// and https:// URLs
  console.log(`${request.method} ${request.url}`);
  return new Response("blocked", { status: 403 });
});

await sandbox.runCommand({
  cmd: "curl",
  args: ["-s", "https://example.com"],
  env: {
    HTTP_PROXY: httpProxy, http_proxy: httpProxy,
    HTTPS_PROXY: httpProxy, https_proxy: httpProxy,
  },
});
```

### HTTPS interception with real fetching

The proxy does TLS MITM — it generates a CA cert at startup, installs
it in the sandbox trust store, and terminates TLS on CONNECT. The JS
handler sees the full decrypted URL and can fetch on behalf of the sandbox:

```ts
const sandbox = await Sandbox.create({
  ports: [5000],
  networkPolicy: "deny-all", // no direct internet access
});
const proxy = createWsProxy();
await proxy.attach(sandbox, { wsPort: 5000 });

const httpProxy = proxy.handle(async (req) => {
  // req.url is "https://vercel.com/robots.txt" — fully decrypted
  // The handler runs OUTSIDE the sandbox, so it CAN fetch
  return fetch(req.url);
});

const result = await sandbox.runCommand({
  cmd: "curl",
  args: ["-s", "https://vercel.com/robots.txt"],
  env: { HTTPS_PROXY: httpProxy, https_proxy: httpProxy },
});
// result.stdout() contains the actual robots.txt content
```

### Per-command security policies

```ts
const allowList = proxy.handle(async (req) => {
  const url = new URL(req.url);
  if (url.hostname === "registry.npmjs.org") {
    return fetch(req.url);
  }
  return new Response("Forbidden", { status: 403 });
});

const allowAll = proxy.handle((req) => fetch(req.url));

// Untrusted code can only reach npm
await sandbox.runCommand({
  cmd: "node", args: ["untrusted.js"],
  env: { HTTP_PROXY: allowList, HTTPS_PROXY: allowList },
});

// Trusted code has full access
await sandbox.runCommand({
  cmd: "node", args: ["trusted.js"],
  env: { HTTP_PROXY: allowAll, HTTPS_PROXY: allowAll },
});
```

### HTTPS CONNECT allow/deny

The optional `connectHandler` controls whether HTTPS connections are
allowed before MITM begins:

```ts
const httpProxy = proxy.handle(
  async (req) => fetch(req.url),
  (host) => host.endsWith(".github.com"), // only MITM GitHub
);
```

### Multiple independent clients sharing one sandbox

```ts
const proxyA = createWsProxy();
await proxyA.attach(sandbox, { wsPort: 5000 }); // starts server

const proxyB = createWsProxy();
await proxyB.attach(sandbox, { wsPort: 5000 }); // reuses existing server

const handleA = proxyA.handle(() => new Response("from A"));
const handleB = proxyB.handle(() => new Response("from B"));

// Requests route to the correct client via session registration
const [a, b] = await Promise.all([
  sandbox.runCommand({ cmd: "curl", args: ["-s", "http://x.com"],
    env: { HTTP_PROXY: handleA } }),
  sandbox.runCommand({ cmd: "curl", args: ["-s", "http://x.com"],
    env: { HTTP_PROXY: handleB } }),
]);
// a.stdout() === "from A", b.stdout() === "from B"
```

## Architecture

- Session routing via HTTP proxy auth (`http://<sessionId>:x@localhost:<port>`)
- JSON protocol over WebSocket (request/response with base64 bodies)
- HTTPS MITM: in-memory CA + per-hostname leaf certs, auto-installed in sandbox trust store
- Multi-client: Go hub tracks session→client mappings via register/unregister
- Config persistence at `/tmp/vercel/http-proxy/config.json` for server reuse

## Tests

- 25 Go unit tests (protocol, hub, proxy server, MITM, session extraction)
- 8 TypeScript process tests (spawn binary, WS client, HTTP/HTTPS proxy)
- 8 sandbox integration tests (real Sandbox, deny-all + HTTPS fetch, multi-client)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant