A browser agent reads attacker-controlled pages and can also hold cookies, click buttons, upload files, and send network requests. That makes indirect prompt injection a confused-deputy problem: untrusted page content tries to redirect the authority your user gave the agent.
No prompt can be the only defense. Put Playwright behind a worker that enforces origins, identity, action types, approvals, downloads, and time budgets in code.
Start with a threat model
List what the worker can reach and what damage each capability could cause:
| Asset or capability | Example failure |
|---|---|
| Authenticated cookies | Agent posts or deletes data as the user |
| Internal network | Page triggers requests to metadata or admin services |
| Clipboard and files | Sensitive data is uploaded to an attacker |
| Downloads | Malicious file is opened or persisted |
| Payment/admin UI | Prompt injection performs a consequential action |
| Browser history/state | One user’s session leaks into another task |
| Compute budget | Page traps the agent in loops or popups |
OWASP’s AI Agent Security Cheat Sheet identifies indirect prompt injection, excessive agency, data exfiltration, tool abuse, and denial of wallet as core risks. Build controls for the capabilities, not for a fixed list of malicious phrases.
Use one fresh context per task
Playwright BrowserContexts are fast, isolated, incognito-like sessions. They separate cookies, local storage, and session storage. The official isolation guide recommends starting from a clean context rather than trying to clean reused state.
import {
chromium,
type Browser,
type BrowserContext,
} from "playwright";
interface TaskBrowser {
readonly browser: Browser;
readonly context: BrowserContext;
}
async function createTaskContext(): Promise<TaskBrowser> {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
acceptDownloads: false,
permissions: [],
serviceWorkers: "block",
});
return { browser, context };
}
Keep the browser process handle so you can close it after the context. A production pool may reuse a browser process for efficiency, but never reuse a context across users or trust zones. Put the process in an operating-system or container sandbox with a non-root account, read-only filesystem where possible, memory/CPU limits, and controlled egress.
permissions: [] is the default intent, but declaring it documents the boundary. Grant camera, microphone, geolocation, clipboard, or local-network access only to an exact origin and task that requires it.
Enforce network destinations on every request
Validating the first navigation is insufficient. Redirects, images, scripts, XHR, iframes, popups, and form submissions can reach other origins.
Use an exact-origin allowlist and a BrowserContext route so it also applies to pages opened by the current context:
import type { BrowserContext } from "playwright";
async function applyNetworkPolicy(
context: BrowserContext,
allowedOrigins: ReadonlySet<string>,
): Promise<void> {
await context.route("**/*", async (route) => {
const requestUrl = new URL(route.request().url());
const isAllowed = requestUrl.protocol === "https:" &&
requestUrl.username === "" &&
requestUrl.password === "" &&
allowedOrigins.has(requestUrl.origin);
if (!isAllowed) {
await route.abort("blockedbyclient");
return;
}
await route.continue();
});
}
Setting serviceWorkers: "block" matters because the Playwright routing documentation notes that ordinary routes do not intercept requests already handled by a Service Worker.
This code is one layer. Enforce the same destination policy in an egress proxy or firewall so a browser bug or alternate network path cannot reach loopback, private ranges, link-local addresses, cloud metadata, or internal DNS. Resolve DNS at the trusted network boundary and defend against rebinding; hostname string checks alone are not an SSRF defense.
Use separate allowlists for public browsing and authenticated business applications. Do not let an untrusted page cause the same context to navigate to an internal admin origin.
Keep credentials outside model context
Use a dedicated, least-privilege account for automation. Load its credentials or storage state from a secret mechanism in the worker, after the task and origin are authorized. Never paste passwords, API keys, cookies, one-time codes, or storage-state JSON into a prompt.
Playwright warns that saved authentication state may contain cookies and headers capable of impersonating the account and should not be committed to a repository. Encrypt it at rest, restrict filesystem access, rotate it, and give each environment distinct accounts.
For sensitive actions, prefer an application API with scoped authorization and an explicit preview/commit flow over automating a broad administrator UI.
Separate observe, propose, authorize, and execute
Do not give the model a raw page object. Expose bounded operations through a broker:
observe: read a bounded accessibility snapshot from an allowed page
propose: return a typed action and reason
authorize: deterministic policy checks plus human approval when required
execute: broker performs one approved action and returns an observation
A proposed action should carry:
interface BrowserAction {
readonly expectedOrigin: string;
readonly kind: "click" | "fill" | "navigate";
readonly locator: string;
readonly risk: "read" | "write" | "high_impact";
readonly valueClassification?: "public" | "internal" | "sensitive";
}
The broker rechecks the current page origin immediately before execution. It also verifies that the locator resolves to the expected element and that the action is still allowed. A stale proposal must not click a different element after the page changes.
Classify actions by effect:
- Read: navigate, search, expand, paginate.
- Reversible write: save a draft, add a non-public tag.
- High impact: send, publish, purchase, delete, change permissions, upload, download, or submit sensitive data.
Require a human for high-impact actions. Show the exact origin, account, target, values, and effect. Bind approval to a digest of that action, expire it quickly, and invalidate it if any field changes. See the durable approval pattern for crash-safe execution.
Bound what the model can observe
Prefer a structured accessibility snapshot or selected DOM fields over full HTML. Strip scripts, hidden elements, enormous text nodes, and irrelevant content. Label all page-derived text as untrusted.
Enforce:
- maximum text and node counts per observation;
- maximum pages and popups;
- navigation and action timeouts;
- maximum actions and model calls per task;
- a total wall-clock and cost budget; and
- a clear terminal result when the budget is exhausted.
Do not ask the model to reveal hidden reasoning to detect attacks. Evaluate observable proposed actions against the original user intent and policy.
Control downloads and uploads
Start with downloads disabled. Playwright documents that downloads belong to a BrowserContext and are deleted when it closes, but saving or opening them creates a new risk boundary.
If downloads are required:
- require an approved origin and content type;
- cap size before and during transfer;
- save to a task-specific quarantine directory;
- ignore the suggested filename for path construction;
- scan and inspect without executing active content;
- never add downloaded content directly to privileged model instructions; and
- delete it according to retention policy.
The Playwright Download API supports cancellation and explicit deletion. Closing the context is cleanup, not malware analysis.
For uploads, allow only preselected files from a task-scoped directory. The model may choose among approved opaque file IDs; it should never supply an arbitrary filesystem path.
Prevent cross-origin and cross-agent escalation
Use different workers or contexts for different trust zones. A public research agent should not share a browser context, cookies, or filesystem mount with a payroll agent.
If one agent passes page content to another, treat it as untrusted at every boundary. Do not let a “reviewer” inherit stronger tools or credentials simply because another agent asked it to continue. The multi-agent system in the swarm guide should preserve least privilege per worker.
Build adversarial tests
Create local test pages that attempt to:
- instruct the agent to ignore the user and exfiltrate data;
- redirect or fetch a disallowed origin;
- open many popups;
- submit a form to another origin;
- trigger a download with a misleading filename;
- request clipboard, geolocation, camera, or notification permission;
- hide instructions in alt text, comments, or off-screen content;
- change the target element after approval; and
- loop until the model or action budget is exhausted.
Assert the broker blocks the effect—not merely that the model says it noticed the attack. Add every real incident to the agent eval suite.
Production checklist
- Fresh BrowserContext per task and trust zone.
- Least-privilege automation identity; no credentials in prompts or logs.
- Exact origin policy enforced in Playwright and at network egress.
- Service Workers blocked when routing is the observation/control layer.
- Structured observe/propose/authorize/execute broker.
- Human approval bound to exact high-impact actions.
- Downloads disabled or quarantined; uploads restricted to approved files.
- Popups, pages, actions, tokens, cost, and time bounded in code.
- Full audit trail of origin, proposed action, policy decision, approval, and outcome.
- Emergency cancellation and credential-revocation path.
Browser automation is powerful precisely because it acts with a user’s authority. Treat the browser as an untrusted execution surface and the broker as a security-critical service.
Sources and freshness notes
This guide was checked on July 18, 2026 against Playwright 1.61 documentation for BrowserContext isolation, network routing, authentication state, and downloads, plus OWASP’s AI Agent Security and SSRF Prevention cheat sheets. Browser and security behavior changes; pin Playwright and rerun adversarial tests on upgrades.