Skip to content

Security

Trust model and access control

Read this before you deploy. The first rule is the one that matters.

The model in one paragraph#

next-live runs code with new Function on your page. That code has the page's full authority: cookies, storage, DOM, and your APIs as the signed-in user.

Safe when snippet authors are people you trust. Unsafe when they are not.

Who can write vs who can view#

1

Trusted author

Writes / saves snippet

Admin, internal dev, your DB write API

2

Your API

Returns source string

Authenticated, audited, versioned

3

LiveProvider

Compiles + runs on page

Same origin, same cookies as the host

4

Visitor

Views preview

Cannot inject code unless they control the write path

RoleCan doYour control
Snippet author (admin, internal dev)Save TSX that runs on your originStrict auth on the write API; audit trail; version history
VisitorView the compiled previewCannot inject code unless they also control the write path
Attacker with URL param accessRun arbitrary JS on your siteBlock this. Never pass user-controlled strings to code

Rule 1: Feed the evaluator only from your API#

tsx
// ✅ from your authenticated API
const { source } = await fetch(`/api/apps/${id}`).then((r) => r.json());
// ❌ never
const source = new URLSearchParams(location.search).get('code');

If an attacker controls what reaches code, no CSP setting prevents execution. That is by design.

Rule 2: Scope unsafe-eval to runner routes#

new Function requires 'unsafe-eval' in script-src. It does not have to apply to your whole app:

lib/runner-routes.ts + proxy.ts
// Shared list: proxy.ts and RunnerLink must agree.
export const RUNNER_ROUTES = ['/playground', '/apps', '/docs'] as const;
export function isRunnerRoute(pathname: string): boolean {
return RUNNER_ROUTES.some(
(route) => pathname === route || pathname.startsWith(`${route}/`),
);
}
// proxy.ts: grant 'unsafe-eval' only when isRunnerRoute(pathname)
// RunnerLink: use <a> (real navigation) for runner destinations

Per-route CSP and client-side routing do not compose. Soft-navigating from / into /docs keeps the landing policy without `'unsafe-eval'`. Use `RunnerLink` for cross-boundary links. Working copy: apps/playground/proxy.ts. Visit /rsc-check to see CSP blocking eval.

The policy also carries 'unsafe-inline' in script-src, because Next injects inline bootstrap and streaming scripts. It is the one real weakening in an otherwise strict policy, worth replacing with a nonce on your non-runner routes, though it buys nothing on the runner routes, where 'unsafe-eval' is already the stronger capability. A nonce-based CSP also forces fully dynamic rendering, which is a genuine cost to weigh.

What is contained (and what is not)#

Contained: runtime errors, render loops. <LiveErrorBoundary> keeps a broken snippet from crashing the host. A render-rate breaker stops runaway setState.

Not contained: full page authority. A synchronous while (true) hangs the tab. window, fetch, and the DOM are always available. The module registry limits convenience, not capability.

If authors stop being trusted#

Same-realm evaluation is for cooperative authors, not a security boundary. Public marketplaces or cross-tenant code need an iframe on a separate origin, a different product with different trade-offs (no live props by reference).

Checklist before production#

  • Snippet source only from authenticated write API
  • Audit log + version history on stored snippets
  • 'unsafe-eval' scoped to runner routes in proxy.ts
  • Strict connect-src: snippets cannot exfiltrate to arbitrary hosts
  • object-src 'none' and base-uri 'self' set, they close known bypasses and cost nothing
  • upgrade-insecure-requests on in production (gated on NODE_ENV so localhost still works)
  • RunnerLink used for every link that crosses into a runner route
  • validateSnippets in CI (Validating in CI)