PromptProductPurchaseFinished Vulnerability Fix

A new vulnerability that may affect your game is if you use PromptProductPurchaseFinished, exploiters are now able to fake / spoof buy a dev product with SignalPromptProductPurchase where you can go call to the server that you have either failed or bought a product, this also affects PromptGamepassPurchaseFinished (make it recheck gamepass ownership when called instead of rewarding) and PromptPurchaseFinished, which may be difficult to fix, depending on your game, as you can call this inside a prompt, this may often affect ugc games.

Example of SignalPromptProductPurchase being used (this is one of my friends not me)

PromptProductFinished vulnerabilities can be fixed by using ProcessReceipt My example for handling UGC games that allows you to buy Free UGCs for robux with a dev product is:

local DEV_PRODUCT_ID =tonumber(game:GetService("ReplicatedStorage"):WaitForChild("DEVPRODUCTID").Value)
local productIdUGC = game:GetService("ReplicatedStorage"):WaitForChild("UGCID").Value

local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local bought = {}

local function processReceipt(receiptInfo)
    local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
    print(player.Name .. " - dev product id", receiptInfo.ProductId)

    local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
    if not player then
        return Enum.ProductPurchaseDecision.NotProcessedYet
    end

    if receiptInfo.ProductId == DEV_PRODUCT_ID then
        bought[receiptInfo.PlayerId] = DEV_PRODUCT_ID
        return Enum.ProductPurchaseDecision.PurchaseGranted
    end

    return Enum.ProductPurchaseDecision.NotProcessedYet
end

MarketplaceService.ProcessReceipt = processReceipt
local success, result = pcall(function()
    MarketplaceService.ProcessReceipt = processReceipt
end)

if not success then
    warn("processreceipt:", result)
end



MarketplaceService.PromptProductPurchaseFinished:Connect(function(player, productId, wasPurchased)
    if tonumber(productId) == DEV_PRODUCT_ID and wasPurchased == true and bought[player] == DEV_PRODUCT_ID then
      pcall(function()  MarketplaceService:PromptPurchase(game.Players:GetPlayerByUserId(player), productIdUGC)
end)
        bought[player] = nil
    end
end)

You can download a copy of my game here where I use processreceipt with PromptProductPurchaseFinished. Dev Product Game Testing - Roblox

Here is my version for my game Be Silent For UGC where I use ProcessReceipt to check multiple dev products at once.

local MarketplaceService = game:GetService("MarketplaceService")

game.ReplicatedStorage.PromptProductPurchase.OnServerEvent:Connect(function(player, productId)
	MarketplaceService:PromptProductPurchase(player, productId)
end)

local item_id = game.ReplicatedStorage:WaitForChild("ITEM_ID", 5).Value

local devproducts = {
	[1788956201] = "Mute All",
	[1788956710] = "Instant UGC",
	[1788956860] = "Instant Win",
	[1788957045] = "Prompt All",
	[1788956431] = "Reset Servers"
}


local Players = game:GetService("Players")
local bought = {}

local function processReceipt(receiptInfo)
	local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
	print(player.Name .. " - dev product id", receiptInfo.ProductId)

	local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
	if not player then
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	if devproducts[receiptInfo.ProductId] then
		bought[receiptInfo.PlayerId] = receiptInfo.ProductId
		return Enum.ProductPurchaseDecision.PurchaseGranted
	end

	return Enum.ProductPurchaseDecision.NotProcessedYet
end

MarketplaceService.ProcessReceipt = processReceipt
local success, result = pcall(function()
	MarketplaceService.ProcessReceipt = processReceipt
end)

if not success then
	warn("processreceipt:", result)
end



MarketplaceService.PromptProductPurchaseFinished:Connect(function(player, productId, wasPurchased)
	productId = tonumber(productId)
	if wasPurchased and devproducts[productId]  and player ~= 116781531 and bought[player] == productId  then
		bought[player] = nil
		if productId == 1788956201 then
			local muteValue = game.ReplicatedStorage:WaitForChild("Mute", 5)
			muteValue.Value = muteValue.Value + 60
		elseif productId == 1788956710 then
			MarketplaceService:PromptPurchase(game.Players:GetPlayerByUserId(player), item_id)
		elseif productId == 1788956860 then
			for i = 1, 3 do
				game.ReplicatedStorage:WaitForChild("Time", 5).Value = 1
			end
			print("Win")
		elseif productId == 1788957045 then
			for _, v in ipairs(game.Players:GetPlayers()) do
				MarketplaceService:PromptPurchase(v, item_id)
			end
			print("Prompt All")
		elseif productId == 1788956431 then
			game:GetService("MessagingService"):PublishAsync("ResetProduct", "Reset " .. tostring(game.Players:GetPlayerByUserId(player).Name))
			print("Reset")
		end
	end
end)

If your game does not prompt anything after a PromptProductPurchase then you do not need the script above, just replace it with ProcessReceipt as PromptProductPurchaseFinished is deprecated and the purchase goes through after the prompt is gone instead of instantly processing.

Roblox does know about this vulnerability but I do not know what they’re doing with it.

Note that you cannot fire these in any normal means, as it is used for corescripts, as you see in Roblox-Client-Tracker. or in the Code Snippet.

if requestType == RequestType.Product then
            local playerId = (Players.LocalPlayer :: Player).UserId

            MarketplaceService:SignalPromptProductPurchaseFinished(playerId, id, didPurchase)
        elseif requestType == RequestType.GamePass then
            MarketplaceService:SignalPromptGamePassPurchaseFinished(Players.LocalPlayer, id, didPurchase)
        elseif requestType == RequestType.Bundle then
            MarketplaceService:SignalPromptBundlePurchaseFinished(Players.LocalPlayer, id, didPurchase)
        elseif requestType == RequestType.Asset then
            MarketplaceService:SignalPromptPurchaseFinished(Players.LocalPlayer, id, didPurchase)

            local assetTypeId = state.productInfo.assetTypeId
            if didPurchase and assetTypeId then
                -- AssetTypeId returned by the platform endpoint might not exist in the AssetType Enum
                pcall(function() MarketplaceService:SignalAssetTypePurchased(Players.LocalPlayer, assetTypeId) end)
            end
        elseif requestType == RequestType.Premium then
            MarketplaceService:SignalPromptPremiumPurchaseFinished(didPurchase or purchaseError == PurchaseError.AlreadyPremium)
        elseif requestType == RequestType.Subscription then
            MarketplaceService:SignalPromptSubscriptionPurchaseFinished(id, didPurchase or purchaseError == PurchaseError.AlreadySubscribed)
        end

All these below are vulnerabilities, if you use the function minus the Signal, please be careful!

  1. SignalAssetTypePurchased
  2. SignalClientPurchaseSuccess
  3. SignalMockPurchasePremium
  4. SignalPromptBundlePurchaseFinished
  5. SignalPromptGamePassPurchaseFinished
  6. SignalPromptPremiumPurchaseFinished
  7. SignalPromptProductPurchaseFinished
  8. SignalPromptPurchaseFinished
  9. SignalPromptSubscriptionPurchaseFinished
  10. SignalPromptSubscriptionCancellationFinished

A video of the vulnerability (Game)

p.s roblox pls make a working processreceipt for gamepasses and regular asset purchases

29 Likes

that seems like a pretty bad thing for roblox to have they should probably fix that huh

1 Like

PromptProductPurchaseFinished is deprecated so doubt, you cant access SignalPromptProductPurchaseFinished in normal means as its used for corescripts

2 Likes

i feel like Roblox should just delete this function or at least limit it to the point that it can’t be used to handle purchases. It can be confusing for unexperienced people when they learn it

1 Like

Thanks, been trying to find a secure way to handle purchases.

I don’t know if am missing something important but from what I seen on the scripts you provided it seems you rely on PromptProductPurchaseFinished event to grant player’s purchases. Now correct if am wrong but YOU SHOULDN’T USE PROMPTPRODUCTPURCHASEINISHED to rely on granting player’s purchased items.

It states You can use this event to detect when a purchase prompt is closed, but it should not be used to process purchases; instead use MarketplaceService.ProcessReceipt.

Most of these “hacks/exploits” seem to work just because the developer is an ignorant. Always double check the api in sensitive cases like these.

5 Likes
didn't read the second or third paragraph

Yes that is correct, but some UGC games “tax evade” because of the obsurd 20000 robux fees to publish a paid limited, so the alternative is use dev products, but ProcessReceipt processes instantly after bought, but PromptProductPurchaseFinished processes after you press “Ok” on the buy button.

You are correct
Though this isn’t just a vulnerability with PromptProductPurchaseFinished, theres multiple that this is affected.
It is much simpler to use PromptProductPurchasedFinished compared to ProcessReceipt.

I don’t see this as vulnerability, this is just a learning issue. You’ve used the wrong event, the PromptProductPurchaseFinished is for interfaces.

The callback with ProcessReceipt is made because a developer product isn’t stored in the player’s inventory, unlike gamepasses & assets. If you call NotProcessedYet, Roblox will call the callback indefinitely to ensure reliability. This is unnecessary for gamepasses & assets.

bruh lol

2 Likes

You can’t recheck with MarketplaceService:UserOwnsGamePassAsync because it caches as explained here:

If the pass is purchased in-experience through PromptGamePassPurchase(), this function may return false due to the caching behavior.

Furthermore, Roblox has official documentation that implements the PromptGamePassPurchaseFinished event handler without rechecking, instead relying on the wasPurchased parameter.

See above

2 Likes

You can fake WasPurchase with SignalPromptGamepassPurchaseFinished…

Yes I know. I’m saying that the official documentation uses an implementation that is not secure. My point is that the MarketplaceService API does not provide a way to verify the in-game purchase of a gamepass that is both reliable and secure.

2 Likes

Do you have the source code for any of this??

game.MarketplaceService:SignalAssetTypePurchased(ticket: string, playerId: int64, productId: int64)
game.MarketplaceService:SignalPromptBundlePurchaseFinished(player: Instance, bundleId: int64, success: bool)
game.MarketplaceService:SignalPromptGamePassPurchaseFinished(player: Instance, gamePassId: int64, success: bool)
game.MarketplaceService:SignalPromptProductPurchaseFinished(userId: int64, productId: int64, success: bool)
game.MarketplaceService:SignalPromptPurchaseFinished(player: Instance, assetId: int64, success: bool)

^^ According to the API

For Example, faking a successful PromptProductPurchase would be

game.MarketplaceService:SignalPromptProductPurchaseFinished(game.Players.LocalPlayer.UserId,1797531211,true) 
1 Like

This sucks that means exploiters can get free gamepasses with 1 line of code (rip my money is wasted now because of exploiters until roblox fixes this)

The documentation currently states that it should not be used for handling purchases but it’s not that evident, should most likely have a warning label and should mention that it’s vulnerable to exploits

2 Likes

what i’ve done to patch it for now is immediately re-checking directly with the API using https://inventory.roblox.com/v1/users/{userId}/items/GamePass/{gamepassId}. it doesn’t seem to cache from what i’ve tested, but it’s really inconvenient that people are going to need an entire roblox proxy just to make sure people can’t fake gamepass purchases

3 Likes

Billion dollar company btw, my games economy was almost destroyed. :+1:

1 Like

Bilion dollar company and they struggle fixing this vuln but adding useless topbar that no one asked for to the devforum doesn’t look like a big trouble for them.

1 Like

Roblox has its priorities, this is obviously not important.

Genuinely, this is like one of the many examples of the insane disconnection to the community lol, no response to something this large is actually crazy.

2 Likes


It’s crazy what happens when you read.