Client-side receipt validation is trivially bypassed: any user with a proxy tool (Charles Proxy, mitmproxy) can intercept the platform's receipt response and return a forged success. This is OWASP A07 (Identification & Authentication Failures) applied to entitlement decisions — the access control check runs in an environment the attacker fully controls. CWE-345 (Insufficient Verification of Data Authenticity) is the direct mapping. The business consequence is that paid features become free for any motivated user. Apple deprecated the legacy /verifyReceipt endpoint in favor of App Store Server API v2 (JWT-signed transactions, api.storekit.itunes.apple.com) — apps still using the legacy endpoint face a compounding validation gap as it is phased out.
High because client-side receipt validation is bypassable by any user with a network proxy, allowing paid features to be unlocked without payment.
Move receipt validation to a server you control, or delegate it to a third-party SDK backend (RevenueCat, Adapty, Qonversion). Never make entitlement decisions solely from a success callback in the app process.
The simplest path uses RevenueCat (free tier available) — customerInfo.entitlements.active is server-validated:
// After purchase — check server-validated entitlements, not the purchase callback alone
const customerInfo = await Purchases.getCustomerInfo();
const isPremium = customerInfo.entitlements.active['premium'] !== undefined;
For a custom backend using Apple's App Store Server API v2 (migrate away from the deprecated /verifyReceipt):
// POST /api/validate-purchase (server-side)
const client = new AppStoreServerAPIClient(
privateKey, keyId, issuerId, bundleId, Environment.PRODUCTION
);
const txInfo = await client.getTransactionInfo(transactionId);
Store validated entitlement state in your database keyed to the user ID (src/lib/entitlements.ts or equivalent) — never to the device alone.
app-store-iap-subscriptions.iap-integration.receipt-validationhighapi.storekit.itunes.apple.com/inApps/v1/, the modern v2 endpoint) or the legacy /verifyReceipt endpoint in backend code (Next.js API routes in pages/api/ or app/api/, Express routes, Supabase Edge Functions in supabase/functions/, Cloud Functions). (2) Google's androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/ in backend code. (3) If using RevenueCat, Adapty, or Qonversion: these SDKs handle server-side validation for you — check that customerInfo.entitlements.active is used to gate premium features (this IS server-validated). (4) Client-only pattern (fail): receipt data decoded or verified entirely within the app without a backend call — look for JWSTransaction parsed in the app, or receiptData sent directly to a local validation function rather than a server. Also check for App Store Server Notifications v2 webhook handler in the backend — POST /api/v1/apple/notifications or similar — which validates the signed payload from Apple's server."src/lib/iap/validate.ts decodes the App Store receipt locally without calling a backend — client-side validation is trivially bypassed with a mock receipt" or "Premium features unlocked in PurchaseSuccess callback without any entitlement verification — receipt not validated at all"// After purchase, check server-validated entitlements
const customerInfo = await Purchases.getCustomerInfo();
const isPremium = customerInfo.entitlements.active['premium'] !== undefined;
/verifyReceipt):
// Server: POST /api/validate-purchase
const client = new AppStoreServerAPIClient(
privateKey, keyId, issuerId, bundleId,
Environment.PRODUCTION
);
const response = await client.getTransactionInfo(transactionId);
/verifyReceipt endpoint — migrate to App Store Server API v2 if using the legacy endpoint