text Untitled 9 views
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