The problem with PromptBulkPurchaseFinished and its security

As opposed to purchases for bundles and assets, the PromptBulkPurchase finished must be called from the client and I have no idea why.

But it can be easily exploited, if an user wanted to award money or in-game rewards, making a remote event which would fire to the server in order to grant rewards.

They can fire a remote event, with correct details and there you go. It’s enough to fire the table, asset ids, robux spent and everything which the documentation states.

I’ve used PlayerOwnsAsset to check whether they own the assets or not, and that is an okay solution regarding this, but it would’ve been easier if there was just a server side check built in.

One of my solutions regarding this was to make a receipt system which would generate a unique identifier of the purchase, then send it back to the client and forward it to the server.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local GenerateReceiptID = ReplicatedStorage:WaitForChild("GenerateReceiptID")
local forwardReceipt = ReplicatedStorage:WaitForChild("ForwardReceipt")

local activeTransactions = {}  -- Stores active transactions with their IDs
local receiptIdSet = {}        -- Set to ensure uniqueness of receipt IDs

-- Function to generate a unique receipt ID
local function generateUniqueReceiptID()
	local chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
	local length = 16
	local receiptId = ''
	for i = 1, length do
		local randIndex = math.random(1, #chars)
		receiptId = receiptId .. chars:sub(randIndex, randIndex)
	end
	return receiptId
end

-- RemoteFunction to get a receipt ID
GenerateReceiptID.OnServerInvoke = function(player)
	local receiptId
	repeat
		receiptId = generateUniqueReceiptID()
	until not receiptIdSet[receiptId]  -- Ensure uniqueness
	receiptIdSet[receiptId] = true
	-- Notify relevant servers about the receipt ID
	forwardReceipt:Fire(player, receiptId)
	wait(1)
	return receiptId
end

This way, the server gets the receipt first, then the client, which will fire it over to the server when Bulk purchase is completed.

I made a table to store valid receipts and as soon as it is processed on the other server script, the receipt is removed.

Exploits can not break the wall since the other server script requires the receipt as well, and only takes valid receipts from this server script - not from client.

I do not have amazing understanding of scripting and yes, this can be improved, but this is my method against exploiting bulk purchases for now.

10 Likes

I don’t fully understand why PromptBulkPurchaseFinished has to be called on the client. This seems inconsistent to me with PromptPurchaseFinished, PromptGamePassPurchaseFinished and PromptProductPurchaseFinished. I personally think this should be looked into a little more. Of course there’s the workaround of doing an extra sanity check on the server but using PlayerOwnsAsset does not always give an accurate representation of which assets were purchased as part of the bulk purchase prompt.

In our market game, users can add whole outfits to a basket which they can then checkout in one prompt. The prompt lets you include assets that the user may already own and will therefor not be included in the current purchase prompt but will still return true when PlayerOwnsAsset is called as a sanity check.

We save the amount of robux spent by our customers to give them a ranking on a leaderboard. This vulnerability allows for an easy way to exploit this system by simply including assets that are already owned by the user.

It should also be noted that there’s a Player parameter to the callback for PromptBulkPurchaseFinished which would imply the event would be fired from the server too?

I strongly believe PromptBulkPurchaseFinished should be called from the Server.