1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
|
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
|