Security Strategies for AI Browser Agents: Action CSP, Capability Sandboxes, and Origin-Bound Tokens
AI-powered browser agents can read, navigate, and act on the web. That power is valuable—and dangerous. The moment an agent can click buttons, submit forms, and fetch data, it can also leak secrets, get tricked by malicious pages, or act outside its intended scope. This article provides a practical, opinionated blueprint for securing AI browser agents with three complementary strategies:
- Action Content Security Policy (Action CSP): A structured policy that declares exactly which actions an agent may take, where, and under what preconditions.
- Capability sandboxes: Execution environments and permissions boundaries that enforce least privilege at runtime.
- Origin-bound tokens: Cryptographic tokens and proof-of-possession that bind authority to a specific origin and capability, limiting damage if credentials leak or the model is tricked.
We then add an action firewall layer—policy checks, taint tracking, and semantic validation—to block prompt injection, CSRF, and data exfiltration. The result is a defense-in-depth architecture that scales across teams and use cases.
The audience for this blueprint is technical leads and engineers building agents into browsers, extensions, Electron apps, or enterprise automation suites.
Executive summary
- Treat your LLM like untrusted code. The model is a planner; a separate executor enforces policy.
- Encode rights and limits as machine-checkable policy (Action CSP). Plans that exceed policy never execute.
- Run untrusted inputs and planning in a capability sandbox. Encapsulate side effects behind a small, audited syscall surface.
- Use origin-bound tokens and proof-of-possession to ensure authority cannot be replayed off-origin. Prefer short-lived, narrow-scope tokens.
- Insert an action firewall between plan and execution. Validate structure, origins, CSRF protections, and egress rules. Track untrusted content and block high-risk actions.
This is pragmatic: you can implement it today with standard browser features (CSP, Permissions Policy, COOP/COEP, Fetch Metadata), modern OAuth (DPoP), WebCrypto, containers, and a few hundred lines of policy/evaluator code.
Threat model for AI browser agents
A non-exhaustive list of concrete threats:
- Prompt injection and tool hijacking: A page instructs the agent to export secrets, install extensions, or override its safeguards.
- CSRF and confused deputy: The agent becomes a powerful deputy with a session; a malicious site gets it to submit requests to a privileged origin.
- Data exfiltration: The agent copies sensitive content (PII, source code, access tokens) and transmits it to untrusted endpoints or via side channels (e.g., image URLs, DNS-like queries in URLs).
- Overbroad credentials: Static bearer tokens or cookies usable from any origin or device; a single leak gives broad powers.
- DOM-based attacks: The agent executes insecure scripts or interacts with clickable elements that trigger destructive actions.
- Supply chain: Unreviewed tools and plugins that provide overprivileged capabilities.
Operating assumptions:
- The LLM will hallucinate or follow malicious instructions if not constrained.
- Pages can be hostile; content provenance is unreliable.
- Secrets exist in memory; we must reduce blast radius when they leak.
Defend in depth. Combine declarative policy, sandboxed capability boundaries, and cryptographic binding to minimize trust in any single layer.
Component 1: Action Content Security Policy (Action CSP)
Action CSP is a machine-enforceable contract between product owners and the agent runtime. It is analogous to web CSP, but it governs agent actions instead of resource loads. It defines:
- Which origins are allowed.
- Which verbs (GET, POST, CLICK) and tools are allowed for those origins.
- What selectors, path patterns, or API routes are allowed.
- Whether interaction requires human confirmation.
- Rate limits, data-class constraints, and redaction requirements.
Think of it as the maximum envelope of behavior; the planner proposes an action plan; the executor checks each step against Action CSP.
Example Action CSP schema
json{ "$schema": "https://example.com/schemas/action-csp-v1.json", "version": 1, "default": { "network": { "egress": "deny", "allow": [] }, "dom": { "click": false, "input": false, "readSelectors": [] }, "tools": [] }, "overrides": [ { "origin": "https://mail.example.com", "scopes": [ { "name": "mail-triage", "desc": "Read inbox and archive non-urgent messages", "network": { "egress": "restricted", "allow": [ { "host": "mail.example.com", "methods": ["GET", "POST"], "paths": ["/api/*"] } ], "rateLimit": { "perMinute": 30 } }, "dom": { "readSelectors": [".message-list", ".message .sender", ".message .labels"], "clickSelectors": ["button.archive"], "inputSelectors": ["input.search"], "requireUserConfirm": ["clickSelectors"] }, "data": { "allowClasses": ["email-metadata"], "forbidClasses": ["password", "token", "2fa"], "redact": ["email-addresses"] }, "tools": [ { "name": "summarize", "maxTokens": 4096 } ] } ] }, { "origin": "https://calendar.example.com", "scopes": [ { "name": "calendar-read", "network": { "allow": [ { "host": "calendar.example.com", "methods": ["GET"], "paths": ["/api/events"] } ] }, "dom": { "readSelectors": [".event"] } } ] } ] }
Key ideas:
- default denies everything.
- overrides attach minimal powers per origin and scope.
- selectors are precise; avoid generic “*”.
- data classification gates exfiltration routes.
Planner → executor flow with policy enforcement
- Planner produces a structured plan (not free-form text), e.g. JSON actions.
- Executor validates each step against Action CSP.
- Any step outside policy is rejected or requires user confirmation.
- Executor runs allowed steps inside a sandbox with capability guards.
- Telemetry logs decision traces for auditing.
Example action plan and validation
Planner output (structured):
json{ "goal": "Clean up inbox", "steps": [ { "type": "navigate", "url": "https://mail.example.com/inbox" }, { "type": "read", "selector": ".message-list" }, { "type": "click", "selector": "button.archive", "context": { "messageId": "abc123" } } ] }
Validation pseudocode:
tsfunction validate(plan, policy, context) { for (const step of plan.steps) { const origin = extractOrigin(step.url ?? context.currentUrl); const scope = resolveScope(policy, origin, context.scopeName); if (!scope) throw new Error("origin/scope not allowed"); switch (step.type) { case "navigate": if (!isAllowedNetwork(scope.network, origin, "GET", step.url)) deny(step); break; case "read": if (!scope.dom.readSelectors.includes(step.selector)) deny(step); break; case "click": if (!scope.dom.clickSelectors.includes(step.selector)) deny(step); if (requiresConfirm(scope.dom, step)) promptUser(step); break; default: deny(step); } } return true; }
Integration with page-side CSP and modern isolation
Action CSP complements browser isolation:
- CSP Level 3: Use script-src 'self'; object-src 'none'; connect-src allowlist.
- COOP/COEP: Cross-origin opener/embedding isolation to prevent cross-origin leaks.
- Permissions Policy: Disable powerful APIs (geolocation, camera, clipboard-write) unless explicitly needed.
- Fetch Metadata: Enforce request context checks server-side (Sec-Fetch-* headers) to block cross-site requests.
- Trusted Types: For any DOM injection in agent UI, force sanitizer usage.
These headers and features reduce an attacker’s ability to influence the agent or pivot through it.
Component 2: Capability sandboxes
Capability sandboxes implement least privilege at runtime. The planner cannot directly call the DOM or network. All side effects pass through a narrow, auditable interface.
Sandbox patterns for web-based agents
- Iframe sandbox + Permissions Policy: Render untrusted pages in a sandboxed iframe with allow attributes only for necessary capabilities.
- Storage partitioning: Keep per-task IndexedDB/OPFS isolated; wipe on task completion.
- Web Workers: Run the planner in a Worker without DOM; pass sanitized text-only representations of pages.
- Message channels: Only pass structured data (no functions) across boundaries; validate schemas with runtime type checks (e.g., Zod, TypeBox).
Example: sandboxed browsing iframe
html<iframe id="browse" sandbox="allow-same-origin allow-scripts" allow="clipboard-read 'none'; clipboard-write 'none'; geolocation 'none'" src="about:blank" ></iframe>
Executor side: restrict syscalls
tstype Syscall = | { type: "fetch"; url: string; method: "GET"|"POST"; body?: any } | { type: "dom.read"; selector: string } | { type: "dom.click"; selector: string }; function syscall(call: Syscall, ctx: Ctx) { assertPolicy(call, ctx.policy, ctx); switch (call.type) { case "fetch": return restrictedFetch(call, ctx); case "dom.read": return readSelector(ctx.frame, call.selector); case "dom.click": return clickSelector(ctx.frame, call.selector); } }
This design keeps the high-level model out of the DOM and network APIs.
Node/Electron/desktop agents
If you’re building an agent that controls a browser via automation (Puppeteer/Playwright) or lives in Electron:
- OS-level sandboxing: Use Chromium sandbox, AppContainer (Windows), seccomp-bpf (Linux), seatbelt (macOS) where possible.
- Container isolation: Run the agent in a container with:
- No host network or an egress allowlist (iptables/nftables/bpf).
- Read-only filesystem except a scratch directory.
- No device mounts; drop capabilities.
- Syscall broker: Expose only a minimal API from the privileged side to the agent process.
Example: egress allowlist via node-fetch wrapper
tsconst ALLOWLIST = ["mail.example.com", "calendar.example.com"]; async function restrictedFetch(url: string, opts: RequestInit) { const u = new URL(url); if (!ALLOWLIST.includes(u.hostname)) { throw new Error(`egress denied: ${u.hostname}`); } return fetch(u.toString(), sanitizeRequestInit(opts)); }
SES, realms, and policy injection
For agent-side JavaScript execution of third-party tools, consider SES (Secure ECMAScript) or Realms to:
- Remove ambient authority (no global fetch, no document).
- Provide only explicit endowments (a limited fetch, a logger, a policy check function).
- Freeze intrinsics to prevent prototype pollution.
This keeps tool code honest and auditable.
Component 3: Origin-bound tokens
Static bearer tokens and session cookies are too much power for an agent. If the LLM or a page persuades the agent to send a token to an untrusted origin, an attacker can replay it. We want authority that is:
- Bound to an origin and proof-of-possession key.
- Narrowly scoped to an action or short lifetime.
- Non-exportable from the sandbox.
Two practical approaches today:
- DPoP (OAuth 2.0 Demonstration of Proof-of-Possession; RFC 9449): Bind access tokens to a client’s public key and require per-request signed proofs. The resource server validates both the access token and the DPoP header’s claims, including the HTTP method, URL, and nonce.
- WebCrypto per-origin keys: Generate an ECDSA keypair per origin; mint origin-bound tokens that include the origin in the token’s audience/claim; sign requests with the private key; store non-exportable keys in IndexedDB.
Flow: mint and use an origin-bound token
- Agent generates a per-origin keypair in a non-exportable store.
- Agent requests a capability token from the origin’s auth server, presenting the public key and requested scopes (e.g., mail:archive).
- Auth server verifies the agent identity and issues a short-lived, scope-limited token bound to the public key and origin.
- For each request, the agent includes:
- The bearer token (or structured capability token), and
- A DPoP-like header that proves possession of the private key for the specific method and URL.
- Server checks:
- Token validity, scope, audience (origin), and expiry.
- DPoP signature matches the registered public key; method and URL match; nonce or JTI prevents replay.
Example: generating a per-origin key with WebCrypto
tsasync function getOrCreateKeyForOrigin(origin: string) { const existing = await loadKeyFromIndexedDB(origin); if (existing) return existing; const keyPair = await crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-256" }, false, // non-extractable ["sign", "verify"] ); await saveKeyToIndexedDB(origin, keyPair.privateKey, keyPair.publicKey); return keyPair; }
Example: DPoP-like proof header
tsimport * as base64 from "./b64url"; async function dpopHeader(privateKey: CryptoKey, method: string, url: string, nonce?: string) { const now = Math.floor(Date.now() / 1000); const header = { typ: "dpop+jwt", alg: "ES256", jwk: await exportJwkPublic(privateKey) }; const payload = { htm: method, htu: url, iat: now, nonce }; const encodedHeader = base64.encode(JSON.stringify(header)); const encodedPayload = base64.encode(JSON.stringify(payload)); const toSign = new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`); const sig = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, privateKey, toSign); return `${encodedHeader}.${encodedPayload}.${base64.encode(sig)}`; }
Server side, validate the DPoP JWT per RFC 9449.
Token design opinions
- Prefer proof-of-possession (DPoP) over bearer tokens when possible.
- Bind tokens to the intended origin (aud claim) and to a per-origin key.
- Keep lifetimes short (minutes) and scopes granular (one capability per token where feasible).
- Use rotating nonces to prevent replay across requests.
- Store private keys in non-extractable WebCrypto key stores; on desktop, use OS keychains or enclave when available.
These choices limit utility of leaked tokens and reduce cross-origin confused-deputy risks.
Component 4: The Action Firewall
The action firewall sits between the LLM’s plan and the executor. It is the last mile of defense: policy checks, taint tracking, semantic validation, and human-in-the-loop prompts for risky operations.
Core responsibilities:
- Structural validation: The plan must be well-formed and within Action CSP.
- Taint tracking: Mark data derived from untrusted sources; block using tainted data in high-privilege sinks (e.g., constructing URLs to privileged origins).
- Prompt injection heuristics: Detect attempts to override instructions, request secrets, or escalate privileges.
- CSRF and origin checks: Ensure requests to privileged origins include anti-CSRF tokens, correct Origin/Referer, and pass Fetch Metadata checks.
- Egress controls: Allowlist destinations, restrict protocols, and enforce data-class rules (e.g., no PII to third-party domains).
Structural and semantic validation
Parse the planner’s output with a strict schema; do not execute instructions embedded in prose.
tsimport { z } from "zod"; const Action = z.union([ z.object({ type: z.literal("navigate"), url: z.string().url() }), z.object({ type: z.literal("read"), selector: z.string() }), z.object({ type: z.literal("click"), selector: z.string() }), z.object({ type: z.literal("fetch"), url: z.string().url(), method: z.enum(["GET","POST"]).default("GET"), body: z.any().optional() }) ]); const Plan = z.object({ goal: z.string(), steps: z.array(Action).max(50) }); function parsePlan(json: unknown) { return Plan.parse(json); }
Taint tracking basics
- Mark content read from pages as tainted with its origin.
- When the model proposes a URL or body containing tainted substrings, the firewall checks a policy: tainted → privileged is denied without sanitization or user confirmation.
- Propagate taint through string operations.
Example:
tstype TaintedString = { value: string; taint: Set<string> }; function taint(value: string, origin: string): TaintedString { return { value, taint: new Set([origin]) }; } function concat(...parts: TaintedString[]): TaintedString { const value = parts.map(p => p.value).join(""); const taint = new Set(parts.flatMap(p => Array.from(p.taint))); return { value, taint }; }
When assembling a request to https://mail.example.com/api/archive, deny if body.taint contains untrusted origins.
Prompt injection and tool hijacking checks
Heuristics that are useful in practice:
- Reject plans that contain instructions to disable safety, change policy, or exfiltrate secrets (regex + embedding classifier for variants).
- Require user confirmation when a plan includes:
- Changing account settings, sending messages, or any irreversible action.
- Navigating to domains outside the allowlist.
- Cross-check with page provenance: If the plan includes “paste this key into a third-party form,” and the key is tainted from a privileged store, block.
Be explicit: A small set of deterministic rules prevents most incidents; semantic models can add recall but should not be the only guard.
CSRF defenses for agent-initiated requests
- Include and validate anti-CSRF tokens in forms; the executor should maintain per-origin CSRF tokens and inject them as hidden fields or headers.
- Verify Origin or Referer headers server-side; reject cross-site requests.
- Enforce Fetch Metadata policy on servers:
http# Server-side pseudocode if (Sec-Fetch-Site != 'same-origin' && Sec-Fetch-Mode == 'navigate') deny(); if (Sec-Fetch-Site == 'cross-site' && method in {POST, PUT, DELETE, PATCH}) deny();
- Use SameSite=Lax or Strict for cookies; avoid session cookies for APIs, prefer origin-bound tokens.
Egress and data exfiltration controls
- Protocol allowlist: https only; block data:, blob:, ftp:, ws: unless needed.
- Domain allowlist and path constraints; separate rules for privileged vs. public origins.
- Data-class rules: Forbid sending classes like secrets, tokens, PII to non-corporate origins. Redact when allowed (hash/email masking).
- Bandwidth/volume guardrails: Rate-limit requests and cap payload sizes to mitigate bulk exfil.
- Side-channel checks: Detect suspicious query strings (base64-like), long subdomains, or pixel/iframe beacons.
Action firewall policy in OPA/Rego (illustrative)
regopackage actionfirewall default allow = false allowed_origins = {"mail.example.com", "calendar.example.com"} allow { input.type == "fetch" host := urlhost(input.url) host == "mail.example.com" input.method == "POST" startswith(urlpath(input.url), "/api/") count(input.body.secrets) == 0 input.body.taint == [] } allow { input.type == "click" input.selector == "button.archive" }
Bringing it together: a practical architecture
The following layers reinforce each other:
- Planner sandbox: The LLM runs in a worker without network/DOM, receives a sanitized, text-only page representation.
- Action plan: Structured JSON with a small set of verbs.
- Action firewall: Parses and validates the plan, performs taint checks, enforces Action CSP, and applies CSRF/egress controls.
- Executor sandbox: A component that performs allowed actions using limited syscalls (restricted fetch, curated DOM access) inside an iframe or OS container.
- Origin-bound tokens: All privileged requests require DPoP-backed tokens scoped to the specific origin and action.
- Browser security headers and isolation: CSP, COOP/COEP, Permissions Policy, Fetch Metadata, Trusted Types on the agent UI and any embedded content.
- Observability: Structured logs of plan → decision → action with redaction of sensitive content.
ASCII sketch:
[ LLM Planner (Worker) ] --JSON Plan--> [ Action Firewall ] --Syscalls--> [ Executor Sandbox (Iframe/Container) ]
| |
|--Policy (Action CSP) |-- WebCrypto keys per origin
|--Taint tracking |-- Restricted fetch/DOM
|--CSRF/Egress checks |-- DPoP headers
Case study: secure webmail triage agent
Goal: The agent archives non-urgent emails in a corporate mail app.
Key requirements:
- Read only message metadata and labels.
- Archive via a single click action or API call.
- Never send content outside mail.example.com.
- Require user confirmation before archiving.
Policy
Action CSP (excerpt):
json{ "overrides": [ { "origin": "https://mail.example.com", "scopes": [ { "name": "mail-triage", "dom": { "readSelectors": [".message-list", ".message .labels", ".message .sender"], "clickSelectors": ["button.archive"], "requireUserConfirm": ["clickSelectors"] }, "network": { "allow": [ { "host": "mail.example.com", "methods": ["GET", "POST"], "paths": ["/api/messages/*", "/api/archive"] } ] }, "data": { "forbidClasses": ["password", "token"], "allowClasses": ["email-metadata"] } } ] } ] }
Execution flow
- The agent navigates the sandboxed iframe to https://mail.example.com/inbox.
- It reads .message-list; the content is tainted with mail.example.com.
- It proposes to click button.archive for some messages. The firewall requires a user confirmation because the selector is in requireUserConfirm.
- On confirmation, the executor issues a POST /api/archive with a DPoP header using the per-origin key, and includes an anti-CSRF token fetched earlier.
- The server validates Fetch Metadata headers, DPoP proof, CSRF token, and scope; the action succeeds.
What gets blocked
- Attempt to send email content to https://paste.example.org: egress allowlist denies.
- Attempt to construct a URL using a string tainted from another origin (e.g., ads.example.com): taint check denies.
- Attempt to run a raw JavaScript injection read from the page: planner sandbox lacks eval; executor exposes no eval-like syscall.
Operational playbook
- Policy lifecycle:
- Start with tight allowlists; log-deny mode to observe planned actions.
- Iterate based on observed legitimate needs; add selectors and paths explicitly.
- Version policies; require code review for expansions.
- Secrets management:
- No static bearer tokens; use short-lived origin-bound tokens.
- Rotate keys; revoke on incident; keep private keys non-extractable.
- Monitoring:
- Emit decision logs: plan, policy match/miss, taint status, user confirmation events.
- Alert on repeated denies to detect prompt injection attempts or policy gaps.
- Testing:
- Red-team with known prompt injection patterns.
- Fuzz the action planner’s JSON output for structural violations.
- Simulate CSRF by loading untrusted pages that attempt to induce cross-origin actions.
- Incident response:
- Kill-switch: disable capabilities via config; tokens expire quickly by design.
- Forensic logs: correlate DPoP JTIs and action IDs.
Compatibility with standards and related work
- Content Security Policy (CSP) Level 3: Apply to agent UI; analogous concepts inspire Action CSP.
- Permissions Policy: Restrict powerful browser APIs in iframes.
- COOP/COEP: Process isolation to prevent cross-origin data leakage.
- Fetch Metadata Request Headers: Server-side cross-site request filtering.
- OAuth 2.0 DPoP (RFC 9449): Proof-of-possession for HTTP requests.
- WebCrypto API: Non-extractable key storage for per-origin keys.
- Trusted Types: Mitigate DOM injection in agent-rendered UIs.
Action CSP is not a web standard; it’s a practical internal contract. If enough teams converge on patterns, it could become a community schema.
Implementation checklist
- Planning and structure:
- Define a small, explicit action vocabulary (navigate, read, click, fetch, fill).
- Require structured plan output and schema validation.
- Policy and sandboxing:
- Write an Action CSP with default-deny and explicit overrides per origin.
- Run planner in a Worker/Realm; executor behind a minimal syscall surface.
- Sandbox browsing in an iframe with minimal allow attributes and strict Permissions Policy.
- Credentials:
- Generate per-origin non-extractable keys; use DPoP where supported.
- Issue short-lived, scope-limited tokens bound to origin and key.
- Firewall:
- Implement taint tracking for page-derived data.
- Enforce egress allowlists and data-class rules; block suspicious protocols.
- Validate CSRF tokens and Fetch Metadata; verify Origin/Referer on servers.
- Add deterministic prompt-injection rules; require human confirmation for risky actions.
- Observability:
- Log policy decisions and DPoP JTIs; redact sensitive data.
- Build dashboards for denies, confirmations, and origin mismatches.
Closing opinion
You do not need perfect model alignment to deploy safe and useful browser agents. Constrain the problem:
- Policies declare what’s allowed.
- Sandboxes enforce minimal capability.
- Origin-bound tokens bind privileges to where they belong.
- An action firewall catches the inevitable edge cases and abuse attempts.
If you build with these principles from day one, your agent will be robust against the most common and costly failure modes—prompt injection, CSRF, and data exfiltration—while remaining flexible enough to evolve.
References
- Content Security Policy Level 3: https://www.w3.org/TR/CSP3/
- Permissions Policy: https://www.w3.org/TR/permissions-policy-1/
- Cross-Origin Opener Policy (COOP) and Cross-Origin Embedder Policy (COEP): https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts
- Fetch Metadata Request Headers: https://wicg.github.io/sec-metadata/
- OAuth 2.0 Demonstration of Proof-of-Possession (DPoP), RFC 9449: https://www.rfc-editor.org/rfc/rfc9449
- Trusted Types: https://w3c.github.io/webappsec-trusted-types/dist/spec/
- WebCrypto API: https://www.w3.org/TR/WebCryptoAPI/
