How do you prevent a malicious parent page from spoofing a payment success event
Never trust client-side events for money. The browser-side defense is origin-checking postMessage and framing protections, but the real answer: payment success must be confirmed server-to-server (webhook / verify call), not via a postMessage the parent can forge.
This question tests whether you understand that the client can never be the source of truth for money.
Why the premise is dangerous
If your payment iframe (or your page embedded in someone else's) signals "payment succeeded" via a client-side event — postMessage, a redirect URL param, a JS callback — anyone can forge it. A malicious parent page can just call iframe.contentWindow.postMessage({ status: "success" }) or open your success URL directly. There's no cryptography in a bare event.
The browser-side mitigations (necessary, not sufficient)
- Validate
event.originon everypostMessagelistener:
window.addEventListener("message", (e) => {
if (e.origin !== "https://payments.trusted.com") return; // reject everything else
// ...
});- Validate
event.source— make sure it's the window you expect. - Framing controls —
X-Frame-Options/ CSPframe-ancestorsso only allowed parents can embed you;Content-Security-Policyto lock down what can talk to you. - Don't put trust in URL params —
?status=successis trivially spoofed.
But all of this only stops casual spoofing. Origin checks stop a page on another domain — they don't stop someone scripting their own page or replaying messages.
The actual answer: server-to-server confirmation
Payment status must come from the payment provider's backend to yours, never through the user's browser:
- The client initiates payment and gets a transaction/session ID.
- The provider (Stripe, etc.) charges and sends a signed webhook to your server — you verify the signature.
- Or your server makes a verify call to the provider's API using the transaction ID: "did this actually succeed?"
- Only after server-side confirmation do you mark the order paid and unlock goods.
- The client-side "success" event is UI only — it shows a spinner/checkmark; it never grants anything.
The framing
"Any client-side event can be forged by whoever controls the page — origin-checking postMessage and frame-ancestors raise the bar but aren't trust. Real payment confirmation is server-to-server: a signed webhook or a verify-API call from my backend to the provider. The browser event is cosmetic; the server decides if money moved."
Follow-up questions
- •How does Stripe webhook signature verification work?
- •Why is checking event.origin not enough on its own?
- •How do you handle the race between the redirect and the webhook arriving?
- •What's the risk of trusting a ?status=success URL parameter?
Common mistakes
- •Treating a postMessage or redirect param as proof of payment.
- •Adding a postMessage listener with no origin check.
- •Unlocking content/goods from a client-side success callback.
- •Assuming X-Frame-Options alone prevents spoofing.
Performance considerations
- •Not a perf question — but the server-verify step adds latency before unlocking, so show an optimistic 'processing' UI while the backend confirms.
Edge cases
- •Webhook arrives after the user already saw the success screen — reconcile state.
- •Webhook never arrives — need a verify-on-load fallback.
- •Duplicate webhooks — handlers must be idempotent.
Real-world examples
- •Stripe sends signed webhook events; your server verifies the signature before fulfilling the order.
- •Embedded checkout widgets that postMessage 'done' purely to update the host UI, with fulfillment gated on the backend.