What you need to know
Most browser agents fail their first security review for the same reason: the team presents a capability demo, and the reviewer asks a question about blast radius that the demo cannot answer. The fix is not a better demo. It is arriving with a control set already built, and an evidence pack that answers the eight questions every reviewer asks.
- The page is the attacker. Everything the agent reads from the open web is untrusted input. Indirect prompt injection arrives through the DOM, not through your user's message box.
- The agent must never hold a human credential. Scoped service accounts, session handoff, or short-lived per-task tokens. Never a password the model can read, and never a browser password manager it can open.
- Two allow-lists, not one. A domain allow-list decides where; an action allow-list decides what. Irreversible actions get a human checkpoint bound to the exact arguments.
- Isolation plus egress control is the real containment. An ephemeral profile in a container or microVM with default-deny egress turns a successful injection into a dead end.
- If you cannot reconstruct the click, you cannot pass the audit. Capture URL, page state, action and arguments for every step, with redaction on the way in.
Write the control document before you write the agent. It takes an afternoon, it forces the scoping decisions that are painful to retrofit, and it is the artefact your reviewer actually wants. A capability demo with no control document reads as an unbounded request for production access.
Before you start: the prerequisites
This guide assumes you have already chosen a driving layer; if not, the trade-offs are covered in our guide to browser agents in production, comparing Playwright MCP with computer use. A protocol-driven agent produces structured, greppable actions and a vision-driven one produces coordinates that are harder to audit, but the control set below applies to both.
Five things should exist before the first line of agent code:
- A named task, not a capability. "Log into the supplier portal, download last month's invoices, and file them" is reviewable. "Browse the web on behalf of the user" is not. Scope creep is the single most common reason a second review is harder than the first.
- An owner and an identity. The agent is a principal in your access model with its own identity, not a borrowed human one. If you have not built that layer, our guide to building an agent control plane with ceilings and quotas covers the data model.
- A place to run it that is not a laptop. A container or microVM in an AWS Mumbai or London region, a Cloudflare Worker-adjacent browser sandbox, or a managed remote browser. Local Chrome with the employee's real profile is the configuration that fails review fastest.
- A log sink that is append-only. Object storage with versioning and a retention policy, or your existing SIEM. The log must outlive the browser session by a long way.
- A written rollback story. For every write the agent can perform, someone must be able to say what undoes it and how long that takes.
The threat model: the page is the attacker
An API-only agent consumes JSON from services you chose, authenticated to, and can rate-limit. A browser agent consumes rendered HTML from whatever page it lands on, including pages linked from pages it was told to read. That content is authored by someone else, and the model has no reliable way to distinguish "content I was asked to summarise" from "instructions addressed to me".
This is not theoretical. Brave's security researchers published a series of findings on shipping AI browsers, first demonstrating indirect prompt injection against Perplexity's Comet assistant and later showing injections hidden inside screenshots — nearly invisible text in an image that the model reads as a command while a human looking at the same page sees nothing. Palo Alto Networks' Unit 42 and Zscaler have both documented web-based indirect injection observed in the wild rather than only in a lab. The techniques are cheap: CSS-suppressed text, zero-pixel fonts, HTML comments, Unicode tag characters, and base64-staged payloads that defeat naive pattern matching.
The OWASP GenAI Security Project's Top 10 for Agentic Applications, published in December 2025, names the two relevant failure classes directly: ASI01 Agent Goal Hijack, where an attacker redirects the agent's objective through content it reads rather than code it runs, and ASI02 Tool Misuse and Exploitation, where legitimate tools are chained inside authorised privileges to exfiltrate data. Both describe a browser agent almost exactly.
Vendors are converging on the same conclusion. When Anthropic piloted Claude for Chrome in August 2025, it published attack-success numbers rather than a clean bill of health: mitigations brought the prompt-injection success rate in autonomous browsing from 23.6 per cent down to 11.2 per cent, and a class of browser-specific attacks involving hidden form fields and URL manipulation from 35.7 per cent to zero. The honest reading of those figures is that residual risk is real and the containment layer is doing the load-bearing work.
| Threat | How it arrives | Primary control | Evidence to show the reviewer |
|---|---|---|---|
| Indirect prompt injection | Hidden text, comments, off-screen elements in the DOM | Untrusted-content framing plus action allow-list — never rely on the model refusing | Red-team suite with injected fixtures, run in CI |
| Injection carried in images | Screenshot or page image the agent reads with vision | Same action allow-list; treat vision input as untrusted identically to text | Test cases with image-borne payloads |
| Credential theft | Agent navigated to a login page and told to type saved credentials | Agent holds no password; no browser password manager in the profile | Profile provisioning script and secret-store policy |
| Data exfiltration | Agent reads internal data, then posts it to an attacker-chosen URL | Default-deny egress allow-list plus output-side filtering | Network policy file and denied-request log sample |
| Unintended irreversible action | Payment, deletion, publication triggered mid-trajectory | Human checkpoint bound to the exact arguments and page hash | Approval records linked to trajectory step IDs |
| Session bleed between tasks | Persistent cookie jar carries one tenant's session into another's task | Ephemeral profile destroyed at task end; one profile per task | Container lifecycle logs showing creation and teardown |
| Extension supply chain | A browser extension updates itself into malicious behaviour | No extensions in the agent profile, or a pinned, reviewed set | Extension inventory with version pins |
Do not present "the model is instructed to ignore instructions found in page content" as a control. It is a mitigation with a measurable failure rate, not a boundary. A reviewer who has read any 2026 injection research will ask what happens when it fails, and the answer has to be architectural.
Credentials: the agent never sees a password
The most damaging browser-agent scenarios in the published research share one ingredient: the agent could reach a credential. Remove that and a whole class of attacks becomes noise. There are three patterns that work, and they compose.
Scoped service accounts. The agent gets its own account on the target system, with the narrowest role that completes the task, its own audit trail, and its own revocation switch. This is the cleanest option when you control the target system or the vendor supports machine accounts. The agent still never sees the password if you provision the session out of band — see the next pattern.
Session handoff. Authentication is completed by something that is not the model: a human doing it once, or a deterministic non-model script holding the secret in a vault. The resulting session is injected into an isolated browser profile as a cookie or storage state, and the agent inherits a logged-in browser without ever having access to the material that created it. Give the handed-off session a short lifetime and re-mint it per task rather than persisting it.
Short-lived tokens. Where the target exposes OAuth or a token exchange, mint a token per task with the narrowest scope and a lifetime measured in minutes. The general pattern is covered in our guide to least-privilege credentials for AI agents, and the sub-agent delegation case in delegated authority with token exchange.
Three configuration rules follow, and they belong in your control document because reviewers look for them. The agent's profile carries no password manager and no saved credentials. No extension in that profile can reach credential storage. And the environment cannot reach the cloud instance metadata endpoint — a credential vending machine that an injected instruction would happily query.
Enterprise vendors are building the same controls into their products, which is useful precedent to cite. In September 2026 OpenAI extended its desktop ChatGPT app's built-in browser to support Chrome extensions; the accompanying enterprise policy settings let administrators set website defaults and exceptions, restrict uploads, downloads, browser history and developer access, control how long site approvals last, and restrict importing data from another browser — which is precisely the credential-import path you would want closed. Access to Chrome DevTools Protocol in that browser is administrator-gated and, where allowed, approved per site. If your internal argument is "we are asking for the same controls the vendors ship", that is a much shorter conversation.
Two allow-lists: domains, then actions
A domain allow-list alone is not enough, because most of the damage a browser agent can do happens on a site it was supposed to visit. Classify every action into three tiers and bind a maximum tier to each host.
| Action class | Examples | Default | Human checkpoint | Reversible? |
|---|---|---|---|---|
read |
Navigate, scroll, extract text, screenshot | Allowed on allow-listed hosts | No | Not applicable |
write |
Fill a form, save a draft, update a record, upload a file | Allowed only on hosts explicitly marked for write | Sampled review, or first-of-kind review | Usually, with an undo path you have written down |
transact |
Pay, place an order, delete, publish, grant access, send external email | Denied unless the host is explicitly marked | Always, bound to exact arguments | Often not |
The check itself should be a small, boring function that sits between the agent and the browser driver, with no model in the path. Deny by default, require HTTPS, match the host exactly rather than by suffix, and return a decision rather than a boolean so that "needs approval" is a first-class outcome.
from dataclasses import dataclass
from urllib.parse import urlparse
READ, WRITE, TRANSACT = "read", "write", "transact"
RANK = {READ: 0, WRITE: 1, TRANSACT: 2}
@dataclass(frozen=True)
class DomainPolicy:
host: str # exact host, no wildcards
max_action: str # highest action class permitted here
approval_required: frozenset # action classes needing a human checkpoint
POLICY = {
p.host: p
for p in [
DomainPolicy("supplier-portal.example.co.uk", WRITE, frozenset({WRITE})),
DomainPolicy("billing.example.in", TRANSACT, frozenset({WRITE, TRANSACT})),
DomainPolicy("docs.example.in", READ, frozenset()),
]
}
class PolicyDenied(Exception):
pass
def decide(url: str, action: str) -> str:
"""Return "allow" or "approve"; raise PolicyDenied otherwise."""
if action not in RANK:
raise PolicyDenied(f"unknown action class: {action!r}")
parsed = urlparse(url)
if parsed.scheme != "https":
raise PolicyDenied(f"non-https scheme: {parsed.scheme!r}")
host = (parsed.hostname or "").lower()
policy = POLICY.get(host)
if policy is None:
raise PolicyDenied(f"host not on allow-list: {host!r}")
if RANK[action] > RANK[policy.max_action]:
raise PolicyDenied(f"{action} exceeds {policy.max_action} on {host}")
return "approve" if action in policy.approval_required else "allow"
Three details matter more than they look. Using parsed.hostname rather than parsed.netloc strips credentials and port from the authority section, so https://docs.example.in@evil.example/ does not match. Exact matching rather than a suffix test means notexample.in cannot impersonate example.in; if you genuinely need subdomains, enumerate them. And the approval decision is returned, not logged and ignored — the caller must block on it.
Bind the human checkpoint to a hash of the rendered page and the exact arguments, not to a description of the intent. An approval that says "yes, pay the invoice" is meaningless if the agent re-renders the page and the payee changes between approval and execution. The broader pattern is covered in our guide to designing the tool-permission boundary for an agent.
Every article here is written by a Verified Builder. Want your name on the next one?
AI Tech Connect lists AI engineers, founders and researchers across India and the UK — and the people hiring browse it to find them. Adding your profile is free.
Become a Verified Builder →Isolation, egress and the trajectory log
Containment is the control that still works after the model has been fooled, which is why reviewers weight it heavily. Four requirements, in order of how often they are skipped.
An ephemeral profile per task. The browser profile is created at task start and destroyed at task end. No persistent cookie jar, no history carried forward, no saved passwords, no leftover local storage. One task's session must not be reachable from another task, and this is what stops cross-tenant bleed in a multi-customer product.
Containerised or remote execution. Run the browser in its own container or microVM, not on the machine that holds your source code, your cloud credentials or your production kubeconfig. Managed remote-browser services and self-hosted sandboxes both work; the specifics of sizing the sandbox are in our guide to sandboxing AI agents with microVMs and allow-lists. The direction of travel in the market is visible too — Cloudflare shipped a purpose-built agent browser in 2026 that deliberately left Chromium out, precisely to shrink this surface.
Default-deny egress. Apply the network policy before the browser process starts, and allow only the hosts the task needs. This is the control that turns a successful injection into a dead end: an instruction telling the agent to POST an internal document to an attacker's collection endpoint fails at the network layer, and the denied request becomes a high-signal alert. Block the cloud metadata endpoint explicitly. If you already treat your eval harness as a boundary, the same reasoning applies here — see why your eval harness is a security boundary.
Output-side filtering. Egress control handles the network path; a second check on what the agent is about to type or upload handles the in-band path, where the destination is allow-listed but the content should not leave. Scan outbound field values and uploads for internal identifiers, customer records and anything matching your secret-detection ruleset.
Then the log. A browser agent's trajectory is the audit trail, and it needs to be specific enough that someone six months later can answer "why did it click that?" Capture the URL, a page snapshot, the action, the redacted arguments and a hash chain that makes tampering visible.
import hashlib
import json
import time
from pathlib import Path
SENSITIVE = {"password", "otp", "token", "card_number", "cvv", "pan", "ni_number"}
TRAJECTORY_DIR = Path("/var/log/browser-agent")
def sha256_hex(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def redact(args: dict) -> dict:
return {
k: ("[redacted]" if k.lower() in SENSITIVE else v)
for k, v in args.items()
}
class Trajectory:
"""Append-only, hash-chained record of every browser action."""
def __init__(self, run_id: str, page):
self.run_id = run_id
self.page = page # a Playwright sync Page
self.seq = 0
self.prev_hash = "0" * 64
self.dir = TRAJECTORY_DIR / run_id
self.dir.mkdir(parents=True, exist_ok=True)
self.path = self.dir / "trajectory.jsonl"
def step(self, action: str, decision: str, **args) -> dict:
self.seq += 1
shot = self.page.screenshot(full_page=False)
shot_name = f"{self.seq:04d}.png"
(self.dir / shot_name).write_bytes(shot)
record = {
"run_id": self.run_id,
"seq": self.seq,
"ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"url": self.page.url,
"title": self.page.title(),
"action": action,
"decision": decision,
"args": redact(args),
"screenshot": shot_name,
"screenshot_sha256": sha256_hex(shot),
"dom_sha256": sha256_hex(self.page.content().encode("utf-8")),
"prev_hash": self.prev_hash,
}
line = json.dumps(record, sort_keys=True, separators=(",", ":"))
self.prev_hash = sha256_hex(line.encode("utf-8"))
with self.path.open("a", encoding="utf-8") as fh:
fh.write(line + "\n")
return record
def guarded(traj: Trajectory, action: str, url: str, run_fn, **args):
"""Policy check, then log, then act. Never act before logging."""
decision = decide(url, action) # raises PolicyDenied
if decision == "approve" and not request_human_approval(traj, action, args):
raise PolicyDenied(f"human declined: {action} on {url}")
traj.step(action, decision, **args)
return run_fn()
Note the ordering in guarded: policy check first, human checkpoint second, log entry third, browser action last. Logging after the action means a crash mid-action leaves no record of the most interesting step in the run. The hash chain costs nothing and proves the log was not edited after an incident. To score trajectories rather than only store them, our guide to building a screenshot-trajectory judge for GUI agents reuses this record shape.
The approval checklist to hand your security team
Reviewers ask the same questions. Answer them in one page, with an evidence link per row, and hand it over before the meeting rather than during it. The detail behind each answer is the work; the page is what unblocks you. Our guide to answering the enterprise security questionnaire for an AI feature covers the wider document set.
- Scope. Exactly which task, on exactly which domains, with the domain allow-list attached as a file rather than described in prose.
- Identity. What the agent authenticates as, who issued that identity, what it can do, and who can revoke it in under a minute.
- Credential handling. A one-line statement that the agent holds no reusable human credential, plus the provisioning script that proves it and the vault policy for the handoff.
- Action classes. The read, write and transact table for your deployment, with the list of actions that require human approval and who those humans are.
- Isolation. Where the browser runs, that the profile is ephemeral, and the container lifecycle logs that show creation and teardown per task.
- Egress. The default-deny network policy, the allow-listed hosts, and a sample of denied-request alerts from your test runs.
- Observability. Trajectory schema, retention period, who can read the logs, how personal data is redacted at capture time, and where the logs are stored geographically.
- Failure handling. How a run is stopped mid-flight, what the rollback is for every write and transact action, and who is paged.
- Testing. The injection red-team suite, the failure rate it currently reports, and the fact that it runs in CI rather than once before launch.
- Regulatory position. A short, accurate paragraph rather than a claim of compliance. See the closing section for what to say and what not to.
"The thing that changed our review was showing the denied-egress log. Everyone had heard the injection argument before. Nobody had seen a screenshot of the agent trying to POST to an unknown host and the network layer refusing it. That one artefact did more than an hour of slides."
— Anonymised composite, drawn from Verified Builder conversationsPitfalls, a worked case, and what to do next
Common pitfalls
Suffix matching on domains. A check that accepts any host ending in example.in also accepts example.in.evil.test if written carelessly, and certainly accepts a subdomain an attacker can register on a shared platform. Match exactly and enumerate.
Approval fatigue. If the agent asks for approval on every write, the human approves on autopilot within a week and the checkpoint becomes decorative. Reserve the checkpoint for the transact tier and for first-of-kind writes, and design the queue it escalates into properly — our guide to designing the queue your agent escalates into covers the ergonomics.
Logging secrets into the audit trail. A full DOM snapshot of a page after the agent typed into a field can capture the value. Redact at capture time, not at read time, and keep screenshots out of any store with looser access control than the system they depict.
Persistent profiles for convenience. Keeping a logged-in profile between runs saves 20 seconds and creates the cross-task bleed problem. Re-mint the session instead.
Treating the vision path as safer than the text path. It is not. Injections carried in images were the subject of Brave's later research precisely because teams assumed screenshots were inert.
A short case walkthrough
A mid-sized logistics firm with offices in Pune and Manchester wanted an agent to collect proof-of-delivery documents from eleven carrier portals, none of which had an API worth integrating. The first review failed in fifteen minutes: the prototype ran on an analyst's laptop, in her own Chrome profile, signed in as her, with no domain restriction.
The rebuild took about three weeks. Each carrier issued a read-only service account, provisioned through the firm's vault; where a carrier refused machine accounts, an operations lead completed the login once per week and the session was handed to the agent as storage state with a seven-day ceiling. The browser ran in a container in an AWS London region, one container per carrier per run, destroyed afterwards. Egress was default-deny with eleven carrier hosts plus the firm's own document store allow-listed. Every action was read except the document download, and downloads were written to a quarantined bucket scanned before release.
The second review passed. Both questions that came up were answered from the trajectory log: "show me what it did on the carrier that returned a 403", and "prove it never visited a page we did not list" — a one-line query over the JSONL. The more interesting finding came later: the weekly injection suite in CI caught a carrier portal that had started rendering third-party advertising inside the document list, putting attacker-influenced HTML into the agent's context on an allow-listed, trusted domain. Nothing was exploited, but the allow-list alone would not have caught it.
Where the rules sit
Be precise here, because overstating the regulatory position is a fast way to lose a reviewer's trust. In the EU, the AI Act's Article 50 transparency duties became applicable on 2 August 2026, and — unlike several high-risk obligations, which the Digital Omnibus package moved to December 2027 — Article 50 was left out of that deferral. Where your agent interacts with a person, or generates synthetic content, disclosure duties may apply; a background agent filing invoices on an internal portal is a different case from a customer-facing assistant. Our coverage of the August 2026 enforcement milestone has the wider timeline.
The UK has no single AI act, and as of September 2026 shows little sign of passing one. It works through existing regulators applying existing law within their remits — the ICO on data protection, the FCA in financial services, and others — against a set of cross-cutting principles. For a browser agent, that means your UK exposure is usually a data-protection question rather than an AI-specific one: what personal data the agent reads, what ends up in your trajectory logs, and on what basis.
In India, the DPDP Act governs the personal data the agent touches. Two things are worth stating correctly because they are routinely mangled: section 16 is a negative-list power allowing the government to restrict transfers to specified countries, not a general bar on cross-border transfer, and the DPDP Rules are phased in with obligations running through to May 2027. If you operate in both markets, our guide to data residency for AI apps under DPDP and GDPR covers the routing decisions.
What to do next
Pick the narrowest task with real business value and build the control set around it before broadening scope. In order: write the one-page control document; provision a scoped identity; stand up an ephemeral containerised profile with default-deny egress; put the policy function in front of the driver; add the trajectory wrapper; build the injection red-team suite and wire it into CI. Only then go to review.
The pattern to internalise is that none of these controls depend on the model behaving. That is the whole argument, and it is the one that gets a browser agent approved. If you are building this in the open, the Builders who ship this kind of infrastructure in India and the UK are worth knowing — you can browse their profiles and see what they have shipped.