Architectural cutaway of an agent-generated button sending an unsafe red path through validation layers toward a browser window while a green path is constrained.

The Button Was the Payload: A2UI CVE-2026-10032

The JSON looked boring: a button, a label, and one function call. In a composite lab scenario, the operator had not asked the agent for code. They had asked it to render an “Open report” action. The detail worth stopping on was the argument passed to openUrl:

{
  "functionCall": {
    "name": "openUrl",
    "args": {
      "url": "javascript:document.body.dataset.marker='executed';void(0)"
    }
  }
}

That value was not markup. It did not need a <script> tag. It was data moving through an agent UI protocol until a browser primitive interpreted it as behavior.

GitHub published CVE-2026-10032 / GHSA-72qq-p3r5-f7wq on October 2, 2026. The advisory assigns CVSS 9.3 and describes agent-controlled openUrl arguments reaching window.open() without a URI-scheme check in @a2ui/web_core versions from 0.9.0 through 0.10.1. Version 0.10.2 is identified as the first patched release.[1]

The bug is small. The security model it exposes is not.

Declarative UI is still an execution surface

A2UI is designed so remote agents can describe interfaces in declarative JSON while the client renders them with native components. The project explicitly frames remote agents and trust boundaries as part of the problem it is solving.[3] This is a sound architectural direction: the agent does not send arbitrary HTML or a JavaScript bundle.

The dangerous inference is that declarative means passive.

A button action is a capability request. A URL is not merely a string once it reaches a navigation API. A function catalog is an interpreter, even when every function has a typed schema. The relevant question is not whether the model emitted code. It is whether model-controlled data can select a capability and shape the arguments delivered to it.

In the vulnerable implementation, the openUrl schema converted the argument to a string. The runtime then called:

if (args.url && typeof window !== 'undefined' && window.open) {
  window.open(args.url, '_blank');
}

The full path documented in the advisory crosses the Button action, generic binder, action dispatcher, data context, catalog invoker, Zod validation, and finally window.open().[1] React, Lit, and Angular renderers all reached the shared web_core behavior. The framework-specific button was not the decisive boundary. The catalog function was.

That distinction matters during review. Auditing JSX for dangerouslySetInnerHTML would miss this class. So would a prompt-injection test that stops after confirming the agent cannot emit raw HTML. The operative source is an agent-supplied function argument; the sink is a browser navigation primitive.

A2UI source-to-sink diagram showing agent data crossing action resolution and string validation before reaching window.open, plus the patched HTTP scheme allowlist.
The vulnerable path validates shape but not navigation semantics. The patch parses the URL, allows only HTTP and HTTPS, and isolates the opener.

What the safe lab proved, and what it did not

I reproduced the source-to-sink condition on October 5, 2026, using a loopback-only page and Chromium 154.0.8037.57. The lab replaced window.open with a recorder, clicked the rendered button, and captured the exact arguments. The first argument retained the javascript: scheme and the second was _blank. The patched function parsed the same value with new URL() and rejected it with Unsupported URL scheme: javascript:.

The reduced vulnerable and fixed functions were:

function vulnerableOpenUrl(url) {
  window.open(url, '_blank');
}

function patchedOpenUrl(raw) {
  const url = new URL(raw, window.location.href);
  if (!['http:', 'https:'].includes(url.protocol)) {
    throw new Error(`Unsupported URL scheme: ${url.protocol}`);
  }
  window.open(url.href, '_blank', 'noopener,noreferrer');
}

The test used only a marker assignment, no network request, credential access, or persistence. It also produced a useful negative result: the native headless Chromium run did not create a new page or execute the marker. That does not invalidate the missing validation. It does constrain the claim. Browser versions, embedded webviews, popup handling, user activation, and host wrappers can alter whether a given javascript: navigation executes.

Treat the advisory as proof of an unsafe capability path, not as evidence that every current Chromium deployment executes the same payload identically. The GHSA calls the outcome stored or reflected XSS.[1] My lab independently verified tainted input reaching the sink and the patch blocking it, but did not reproduce native script execution in that Chromium build.

This difference is operationally important. A scanner that flags window.open(tainted) has found a real trust-boundary defect. An incident report that asserts cross-browser code execution still needs browser-specific evidence.

The attack chain is shorter than the prompt-injection story

The realistic chain has four decisions:

  1. Influence the UI description. A malicious tool result, retrieved document, compromised agent, or direct untrusted request steers the agent toward a crafted button action. The attacker does not need to alter renderer code.
  2. Pass catalog validation. The function name is allowed and the URL argument is a string. Structural validation succeeds because it answers the wrong question.
  3. Wait for user activation. The victim clicks an interface element that looks like an ordinary navigation control. The advisory’s CVSS vector includes user interaction.[1]
  4. Inherit renderer context if the host executes the scheme. Impact then depends on the browsing context, CSP, cookie flags, storage accessible to script, privileged bridges, and whether the renderer runs in a conventional browser or an application shell.

The human factor is precise rather than theatrical. The victim is not persuaded to paste a command into a terminal. They approve an action through the interface the application taught them to trust. The button label, placement, and surrounding content can all be legitimate. Only the capability argument is hostile.

This is where agent-generated UI changes phishing economics. The attacker can place the deceptive action inside a context assembled from real application state. A familiar component library may improve the lie because visual consistency comes from the trusted client renderer, not from attacker-supplied CSS.

The same reasoning applies beyond openUrl: file pickers, clipboard writes, downloads, deep links, tool calls, payment intents, and native bridge methods all turn declarative fields into capabilities.

The patch fixes two boundaries

The June 19 fix resolves the URL against the current location, rejects schemes other than http: and https:, and opens accepted URLs with noopener,noreferrer.[2] It also adds negative tests for schemes including javascript:, data:, file:, ftp:, mailto:, tel:, and blob:.[2]

The order is correct. Validate the parsed URL, not a lowercased prefix. String checks are vulnerable to whitespace, encoding, mixed case, parser differentials, and relative-URL ambiguity. A canonical parser establishes the scheme the browser will use. An allowlist then fails closed.

noopener addresses a separate problem: a newly opened page should not retain an opener reference that can navigate or manipulate the source tab. MDN documents that window.open() creates or reuses a browsing context and that same-origin policy controls how much the opener can interact with the opened context.[4] Scheme validation and opener isolation are complementary, not interchangeable.

One tradeoff deserves attention. Blocking every non-HTTP scheme can break legitimate application deep links. The safe response is not to restore a general string sink. Put each privileged scheme behind a separate, explicit capability with its own policy, confirmation text, telemetry, and platform handler. openWebUrl, openMailComposer, and openApprovedDeepLink should not be aliases for one permissive openUrl.

Detection should follow capability dispatch

Network controls will not see the critical decision if the payload is entirely local. Instrument the dispatcher and catalog boundary.

Useful events include the agent or conversation identifier, component ID, function name, normalized scheme, renderer version, policy decision, and user-activation timestamp. Do not log full URLs indiscriminately because query strings may contain secrets. A scheme plus a keyed hash of the normalized destination is often enough for correlation.

For deployed clients, hunt for:

  • non-HTTP schemes in serialized A2UI actions, saved conversations, or tool outputs;
  • openUrl calls rejected by the new policy;
  • unusual spikes in agent-created buttons that invoke navigation;
  • renderer packages below @a2ui/web_core 0.10.2;
  • application shells exposing privileged native bridges to the renderer origin;
  • CSP reports involving inline script or blocked navigations near agent UI interactions.

CSP remains defense in depth. It should not be asked to repair an interpreter that gives untrusted data to a dangerous sink. Browser policy also says little about a mobile or desktop renderer that maps the same action to a native API.

For red teams, the highest-value test is capability mutation. Enumerate every catalog function, then mutate arguments across scheme, origin, path, MIME type, size, and encoding boundaries. Run the matrix against each renderer and host shell. A function-level schema that accepts string is a starting point for fuzzing, not a security control.

Strategic takeaways

Agent UI security is not primarily a rendering problem. It is a capability-design problem.

Keep the agent’s language narrow, but assume every allowed action can be adversarially composed. Validate at the last responsible moment, where data becomes browser or operating-system behavior. Give high-impact functions distinct names and policies. Preserve the original agent event and the normalized action for investigation. Test the same protocol across browser, mobile, and desktop implementations because shared semantics do not guarantee shared enforcement.

Teams already threat-modeling prompt injection should connect this layer to the rest of the agent stack. The site’s earlier analyses of enterprise AI attack chains, MCP security, and approval-dialog deception in AI coding agents cover adjacent control failures. CVE-2026-10032 adds a crisp rule: when agent data reaches a client capability, the renderer is part of the agent’s security perimeter.

The button was not the payload because it looked suspicious. It was the payload because the system let a string choose what the browser should do.

Sources

  1. A2UI security advisory GHSA-72qq-p3r5-f7wq (published October 2, 2026)
  2. A2UI openUrl scheme validation fix (commit dated June 19, 2026)
  3. A2UI repository and protocol documentation (accessed October 5, 2026)
  4. MDN Window.open documentation (accessed October 5, 2026)

💜 Enjoyed this content? Support the blog with USDT (TRC20):

TX7obcjHQbDUXb4mGqoASEu1QFTKT2CFGG

View support page

Paulo Rigonato

Security Engineer | Red Team | Pentest

Offensive security specialist with experience in assessments, pentesting, and Red Team operations. He works in enterprise cybersecurity and continues to share knowledge through this blog.

Certifications: OSCP | eWPTXv2 | ITILv4

💻 GitHub 🔗 LinkedIn

Similar Posts