SCREECHVR V17: IAP + PURCHASE SECURITY REVIEW Date: 2026-10-30 Scope: Focused read-only source and current official documentation review. No code, revisions, dashboards, or production services changed. V08 is a draft, not verified as deployed. SUMMARY Meta checkout callbacks are currently treated as sufficient to trigger game rewards in the inspected Unity scripts. No custom gameplay caller independently verifies Meta entitlement server-side. SpokesSigmaIAP uses client-writable OWNED_IAP as a duplicate check, and writes it only after rewards; BundleIAP has no comparable dedupe. Whether those callback paths are exploitable in practice depends on platform SDK guarantees, app config, and backend settings, none of which were tested. Meta explicitly provides S2S ownership verification and consumable management for trusted-server fulfillment. 1. META IAP IMPLEMENTATION `! Screech VR/Screech VR/Assets/Scripts/SpokesSigmaIAP.cs` - `OnTriggerEnter` starts `AttemptPurchase()` when purchase is enabled, finger collider enters, and PlayFab ID is present. `AttemptPurchase()` calls `IAP.LaunchCheckoutFlow(SKU)`. - `HandlePurchase()` checks callback error/success; success proceeds to `TransferItem()`. It logs returned `msg.Data.Sku` but does not compare it with configured `SKU` before granting. - `TransferItem()` reads PlayFab `OWNED_IAP`; if the configured SKU is present it declines, otherwise calls `GrantAll()`. - `GrantAll()` uses component-configured `CatalogItemIds` and `currencyToGive`/ configured prices to grant currency and call PlayFab client `PurchaseItem` for each mapped item. `FinishGrant()` activates the Unity reward, then `AppendOwnedSku()` writes configured SKU to `OWNED_IAP` using client `UpdateUserData`. - Confirmed: check/grant/write is not atomic. A crash, partial failure, or concurrent/replayed callback before the final write can duplicate value. The marker is ordinary UserData and is client writable per PlayFab docs. The script’s SKU-to-reward mapping is component/config driven, not independently verified against Meta SKU by a server. `! Screech VR/Screech VR/Assets/Scripts/Atm_Manager.cs` - `InitializeIAP()` loads products and calls `IAP.GetViewerPurchases()`. `GetPurchasesCallback()` only logs history; no reward restore/fulfillment is performed. `AttemptPurchase()` checks configured `skus`; `BuyProductCallback()` compares returned SKU to the requested SKU, then grants configured `currencyToGive` through `Oculus_Login.AddCurrency()`. - Confirmed: purchase history is queried but not used to restore rewards; ATM reward amount is local configuration and the grant is a client currency call. - Related caller: `! Screech VR/Screech VR/Assets/Scripts/ATM_TAB.cs` passes its configured SKU and Currency into `Atm_Manager.AttemptPurchase`. `! Screech VR/Screech VR/Assets/BundleIAP.cs` - `AttemptPurchase()` calls Meta `LaunchCheckoutFlow`; `HandlePurchase()` treats callback success as fulfillment and does not compare returned SKU to the configured value. `TransferItem()` grants one currency through `Oculus_Login.AddCurrency()` and calls PlayFab `PurchaseItem` for configured `PlayfabItemID` at price 1; success activates reward. - Confirmed: no receipt/ownership server verification, no `OWNED_IAP` check/write, and no visible duplicate-request guard in this path. If the configured Meta SKU is consumable or the callback can be replayed/re-entered, duplicate claim risk exists; product type and runtime callback behavior are unverified. Other related ordinary PlayFab purchases: - `Scripts/BuyCosmetic.cs` (`AttemptPurchase` and callbacks), `Scripts/BuyOnCollide.cs` (`Buy`), `Scripts/BuyBundleOnCollide.cs` (`Buy` / `BuyNext`), and `Glitched Cat Studios/Wardrobe/Scripts/GcsWardrobePurchase.cs` (`PurchaseItem`) call PlayFab client `PurchaseItem` for virtual-currency catalog items. These are not Meta store receipts. Bundle purchases iterate missing IDs; failures may leave partial completion and need retry semantics. - `SpokesSigmaIAP` and `BundleIAP` also invoke client `PurchaseItem` after Meta checkout, mixing two payment systems: Meta entitlement plus PlayFab virtual currency purchase. This must be deliberately preserved or replaced as a single server transaction to avoid double charging or partial grants. 2. RECEIPTS, OWNERSHIP, RESTORE, AND REPLAY - Source-confirmed: inspected custom gameplay scripts contain no Meta S2S `verify_entitlement`, `viewer_purchases`, or `consume_entitlement` calls and no PlayFab store-receipt validation invocation for these Meta purchases. SDK method definitions in the project do not prove the game uses them. - `OWNED_IAP` in SpokesSigmaIAP is a comma-separated client UserData string. It can be modified by its owner under PlayFab's documented client UserData model, can be deleted/reset, and is not a transaction ledger. It is written after rewards, so retry after interruption can grant twice. It may prevent an ordinary second click after a successful completion, but is not secure replay protection. - `Atm_Manager.GetPurchasesCallback()` only logs the result from `GetViewerPurchases`; it neither revalidates ownership on the backend nor restores PlayFab reward/currency. `BundleIAP` shows no purchase-history path. - Durable vs consumable type for each configured Meta SKU is not available from source. Meta `GetViewerPurchases()` returns durable items and unconsumed consumables; consumables must be consumed after fulfillment, while durable entitlements should not be consumed. The inspected scripts do not call `IAP.ConsumePurchase`. - Restoration is therefore incomplete in these custom paths: ATM history is observed but not applied; there is no demonstrated secure restore for bundle or Spokes rewards. A client-only ownership list should not mint currency/items. Severity: - High conditional: forged/replayed Meta success causes value if the client callback can be spoofed/replayed or the client process is modified and no S2S verification is performed. The source trusts callback success; actual Meta callback authenticity/tamper resistance and production entitlements were not independently tested. - High source-level reliability/security flaw: non-atomic grant then OWNED_IAP write and lack of server idempotency permit duplicate/recovery ambiguity. - Medium/High conditional: SKU mismatch in SpokesSigmaIAP/BundleIAP could route a success result to another configured reward if SDK/callback inputs can be manipulated; verify the `msg.Data.Sku` check anyway. Atm_Manager does compare the returned SKU. - Medium: incomplete restoration risks losing legitimate entitlements after reinstall/account recovery, and naive client restore could duplicate rewards. - Unknown: actual Meta SKU catalog, consumable/durable classification, store setup, developer feature permissions/DUC, and any external receipt validation. 3. PLAYFAB PURCHASEITEM AND CUSTOM HANDLER DIFFERENCES Ordinary client `PurchaseItem`: - Official Legacy Economy Client API requires ItemId, VirtualCurrency, and the price the client expects. Documentation says this lets PlayFab fail the purchase if price changed; without StoreId, price defaults to catalog. This is server-side catalog price consistency for that API call, not proof of Meta payment and not protection for custom code that bypasses PurchaseItem. - It still relies on correct catalog/store setup and client-provided expected price/currency. The client UI’s affordability checks are convenience only. - API is marked Legacy Economy, bugfix-only; new Economy work is v2. V08 custom handlers: - `revisions/SCREECHVR_PLAYFAB_REVISION_V08_2026-10-29.js`, `OnPurchaseItem` (302-380): uses client `args.Price`, checks/deducts and grants itself; catalog lookup is TODO. It does not call PlayFab `PurchaseItem`, so the official PurchaseItem catalog price check does not apply. Currency may be deducted before grant failure, with no demonstrated transaction rollback/idempotency. - `ServerValidatePurchase` (542-572) is a placeholder that returns “price lookup not yet configured”; it does not validate a store receipt or complete a purchase. - V08 is marked DRAFT - NOT DEPLOYED. Do not attribute its defects to production without verifying deployed revision and caller access. Difference from Meta IAP: Meta checkout charges/entitles the platform account; PlayFab PurchaseItem spends PlayFab virtual currency and grants catalog items. One cannot substitute for the other. A Meta callback followed by client PurchaseItem can fail after Meta charged, and a client-reported callback alone must not authorize backend rewards. 4. SKU / REWARD MAPPING AND SECURE TARGET FLOW Mapping: - Current mapping sources are serialized Unity values: `SKU`, `CatalogItemIds`, `currencyToGive`, `prices`, and `PlayfabItemID`; ATM_TAB supplies SKU/currency. Values are not validated against an authoritative server mapping in inspected code. Meta dashboard products/config were not accessed. - Future backend owns an allowlisted mapping: Meta SKU -> durable/consumable type -> PlayFab item(s)/currency amount -> fulfillment policy. Reject unknown SKU and ignore client-supplied amount, item list, price, and returned SKU unless it matches the authenticated purchase record. Recommended server flow preserving existing products: 1. Unity starts Meta checkout, then sends the authenticated user ID context and SKU to backend. Do not deliver value merely because client callback says success. Backend derives user from verified session, not client-supplied ID. 2. Backend verifies current Meta ownership/entitlement with Meta S2S `verify_entitlement` for that user/SKU, or reconciles `viewer_purchases` with pagination. Keep Meta app secret server-side. 3. Backend loads server-owned SKU/reward mapping and writes a unique transaction record with states such as received/verified/fulfillment-pending/fulfilled. Enforce unique key on Meta purchase/entitlement ID (or safe stable SKU+grant identity where the platform exposes no transaction ID); serialize concurrent attempts. Do not rely on OWNED_IAP as uniqueness authority. 4. Fulfill all configured PlayFab items and currency once. Because Meta verify + PlayFab grant + database cannot be one atomic transaction, use durable idempotent workflow/outbox and retry each unfinished item safely. Mark fulfilled only after authoritative result; return current status on retries. 5. For consumables, consume via Meta S2S only after durable fulfillment succeeds. For durable entitlements, do not consume; reconcile ownership on login and idempotently ensure the mapped reward. If fulfillment fails, keep pending and retry/support rather than consuming or charging twice. 6. For restoration, query Meta ownership server-side and reconcile the backend ledger. Restore durable items idempotently; do not re-grant consumable currency from an old purchase already fulfilled/consumed. Provide an explicit support path for missing entitlements. 7. Unity receives authoritative fulfillment state and refreshes PlayFab inventory/currency; keep UI, cosmetic unlock, and bundle behavior intact. PlayFab virtual-currency shop flow: send only item ID and request ID; resolve catalog price/currency server-side or use PlayFab PurchaseItem with the correct catalog/store values. For bundles, record per-item progress and safely resume without charging/granting twice. Preserve item IDs and current catalog products. 5. DEPENDENCIES, DASHBOARD QUESTIONS, AND FAILURE RECOVERY Dependencies to verify before implementation: - Meta IAP S2S API access, User ID and IAP Data Use Checkup permissions, APK association, app secret storage, product types/SKUs, and available transaction identifiers. Meta docs say a valid APK upload is required for Monetization APIs. - Backend endpoint/database with authenticated PlayFab session verification, durable unique transaction records, retry queue/worker, and audit/retention. - PlayFab title/catalog currency and item configuration; deployed CloudScript revision and callable handlers; legacy PurchaseItem behavior and API policies. - Unity SDK version exposes needed Meta methods; Quest test accounts and store test environment. No dashboard settings were inspected or changed. Failure handling: - Meta S2S outage/network timeout: do not grant; keep pending, retry with bounded backoff, present pending status, and reconcile later. Never mark owned on client callback alone. - PlayFab partial grant: retain transaction pending, retry only missing idempotent fulfillment operations, then consume Meta consumable after all fulfillment. - Duplicate checkout callback/request: return prior transaction status; no second grant or purchase. - User cancellation: no grant. SKU mismatch/unknown product: reject and log safely. Refund/revocation: define policy and reconcile entitlement; do not silently delete unrelated owned items. - Preserve data migration and keep old paths disabled only after staging proves old clients cannot double grant during rollout. 6. REGRESSION TESTS Use Meta test users and PlayFab development title only: 1. Buy every configured SKU through SpokesSigmaIAP, ATM, and BundleIAP; verify exact mapped cosmetic/item/currency and unchanged normal UX. 2. Alter/mismatch callback SKU, unknown SKU, wrong configured amount/item, and replay successful callback/request; backend rejects mismatch and grants once. 3. Force app close/network timeout after Meta purchase, after verification, and mid-PlayFab fulfillment; retry/relaunch reconciles to exactly one grant. 4. Test durable purchase restore across reinstall/device/account relogin; verify entitlement restored without duplicate currency. Test consumable is fulfilled once, consumed after grant, and can be purchased again legitimately. 5. Fail Meta verification, Meta ownership absent, invalid user/SKU, S2S outage, PlayFab grant failure, and consume failure; state remains recoverable and no false success or lost paid entitlement occurs. 6. Test currency shop/cosmetic/bundle paths: changed catalog price, wrong currency, insufficient funds, already-owned item, partial bundle failure, repeated request, and PlayFab callback error. 7. Validate receipts/ownership with backend for correct authenticated account; attempt cross-account ownership query and ensure it is rejected. 8. Verify audit/idempotency ledger and no secrets/receipts in logs. OFFICIAL DOCUMENTATION CONSULTED - Meta Unity IAP: products, checkout, viewer purchases, durable cache, consume, and recommendation to confirm ownership server-side before delivering value: https://developers.meta.com/horizon/documentation/unity/ps-iap/ - Meta IAP S2S: verify entitlement, viewer purchases (pagination), consume: https://developers.meta.com/horizon/documentation/unity/ps-iap-s2s/ - PlayFab Client PurchaseItem (Legacy Economy; expected price compared by service; defaults to catalog when StoreId omitted): https://learn.microsoft.com/en-us/rest/api/playfab/client/player-item-management/purchase-item?view=playfab-rest - PlayFab API docs note Legacy Economy is bugfix-only; new features in Economy v2. No implementation or production changes. This is a focused purchase-security supplement, not a rewrite of V14-V16. END OF REPORT