How to patch exploit that fakes dev product purchases?

  1. What do you want to achieve? Keep it simple and clear!
    I need to make a fix for an exploit that can apparently make the game think people purchased a dev product. (I do not know how this is possible)

  2. What is the issue? Include screenshots / videos if possible!
    this is an issue because it makes exploiters able to get many points in my game.


    the image is an image of the ingame points leaderboard, showing many alt accounts that gained very large amount of points one day, and that shouldn’t be possible to be obtained within such a short time (These exploiter alt accounts were already game banned by me)
    I wanna fix this exploit, it ruins the leaderboards for other players and it is very unfair.

  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    I haven’t seem to have found any solutions to fix this exploit

for information, I have data wiped and banned all the illegitimate players on my game that were found using the exploit.


this image is one that I found on my community server of someone potentially using the exploit, if anyone knows this exploit and how to patch it please let me know!

4 Likes

Can you share the relevant code?

The only way such an exploit can happen is if you have some remote from Client to Server that says “I purchased a dev product” and the Server will trust that remote. Are you listening for the purchase completion on the client? All dev product/gamepass/anything with robux should be handled entirely on the server when validating a purchase.

1 Like

This might actually be a legit exploit on DevProducts since the exploit is scanning which means that it’s universal. I don’t think it uses any remote events.

EDIT: I just found the code and it’s open sourced lmao. I’ll see how they did it.

1 Like

By searching on the internet, I have found this video

it seemed to contain the script, and this is what I found

  getgenv().promptpurchaserequestedv2 = MarketplaceService.PromptPurchaseRequestedV2:Connect(
                        function(...)
                            discord:Notification("Prompt Detected",
                                "If this is a UGC item, this script will attempt purchase. Please check console.",
                                "Okay!")
                            local startTime = tick()
                            t = {...}
                            local assetId = t[2]
                            local info = MarketplaceService:GetProductInfo(assetId)
                            pcall(function()
                                local starttickxd = tick()
                                local data = '{"collectibleItemId":"' .. tostring(info.CollectibleItemId) ..
                                     '","collectibleProductId":"' .. tostring(info.CollectibleProductId) ..
                                     '","expectedCurrency":1,"expectedPrice":' .. tostring(info.PriceInRobux) ..
                                     ',"idempotencyKey":"' ..
                                     tostring(game:GetService("HttpService"):GenerateGUID(false)) ..
                                     '","expectedSellerId":' .. tostring(info.Creator.Id) .. ',"expectedSellerType":"' ..
                                     tostring(info.Creator.CreatorType) ..
                                     '","expectedPurchaserType":"User","expectedPurchaserId":' ..
                                     tostring(game.Players.LocalPlayer.UserId) .. '}'
                                print(data)
                                -- setclipboard(data)
                                _post("https://apis.roblox.com/marketplace-sales/v1/item/" .. tostring(info.CollectibleItemId) .."/purchase-item", data);
                                wait();
                                local endTime = tick()
                                local duration = endTime - startTime
                                print("Bought Item! Took " .. tostring(duration) .. " seconds")
                            end)
                        end)
                    end)

i am not sure what that code does but it may be related to the exploit

To be blunt, the only reason this exploit is a thing is because people don’t read documentation.

The easiest way to fix this is to just use ProcessReceipt for your dev product purchases.

Stop using PromptPurchaseProductFinished to check that a player has finished purchasing a product (or not).

The reason why that exploit “only works on some games” is because those games in question make full use of ProcessReceipt, which does not have that vulnerability.

You should be using it anyways, it’s the most robust interface you have to properly process a purchase.

Hell, you’re literally recommended to use it by Roblox themselves:

Fires when a purchase prompt closes for a developer product. 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.

Also this should go without saying - use ProcessReceipt on the server side.

6 Likes

One more thing, PromptGamePassPurchaseFinished also suffers from the same vulnerability, except this time you can’t use ProcessReceipt.

The fix for this isn’t too difficult either though - check whether the player does actually own the gamepass first before you give the player any benefits of the gamepass, using MarketplaceService:UserOwnsGamePassAsync().

5 Likes

Very cool I didn’t know about this

1 Like

This issue just happened in my game where a player was able to product purchase spam my game because we have an event that would happen everytime someone bought a specific gamepass aka the riot gamepass.

1 Like

@Rusi_002 If you could please contact me my game is also currently suffering this same problem (TVA)

Use ProcessReceipt

local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local products = {
    [12121121221] = {
        name = "VIP MEMBERSHIP FOR EXAMPLE",
        func = function(player)
            assert(player and player:IsA("Player"), "Invalid player")
            assert(player:FindFirstChild("VIPValue"), "VIPValue not found")
          print("ETC")
        end
    }
}

local function ProcessReceipt(receiptInfo)
    assert(type(receiptInfo) == "table", "Invalid receiptInfo")
    assert(type(receiptInfo.PlayerId) == "number", "Invalid PlayerId")
    assert(type(receiptInfo.ProductId) == "number", "Invalid ProductId")

    local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
    if not player then
        warn("Player not found for UserId: " .. receiptInfo.PlayerId)
        return Enum.ProductPurchaseDecision.NotProcessedYet
    end

    local product = products[receiptInfo.ProductId]
    if not product then
        warn("Product not found for ProductId: " .. receiptInfo.ProductId)
        return Enum.ProductPurchaseDecision.NotProcessedYet
    end

    local success, err = pcall(function()
        product.func(player)
    end)

    if not success then
        warn("Error processing product: " .. product.name .. " for player: " .. player.Name .. ". Error: " .. tostring(err))
        return Enum.ProductPurchaseDecision.NotProcessedYet
    end

    return Enum.ProductPurchaseDecision.PurchaseGranted
end

MarketplaceService.ProcessReceipt = ProcessReceipt

Correct. People don’t read that documentation unless they literally have to. All of us learn how to develop through many other resources including this dev forum. Documentation teaches you the bare minimum and is mostly just useless examples instead of choosing examples from popular games that actually demonstrate when and how something should be used.

2 Likes

Fun fact. With enough time and effort, you could learn more from picking apart their FPS system template than you can from the entire FPS system documentation combined…

1 Like

So I did the whole ProcessReceipt thing and yet I am still having the same issue where they are still somehow able to spoof purchases.

1 Like

Do not trust the client in checks make sure all your checks in serverside

3 Likes

All the checks are server sided, the ProcessReceipt and everything happens on the server, which is why it’s weird and confusing. The current setup is like this:
So I just can’t find what they are doing and how they are making it so that the stuff runs, and in specific although I won’t show it there is a function similar to these two tool givers that runs a riot that they are specifically targeting that I can’t figure out how if this system is exactly the same to what the riot runs off of and its all done within one server script.

– Server –

productFunctions[1671135153] = function(_product,player)
	game.ReplicatedStorage:WaitForChild("Events"):WaitForChild("LogBindable"):Fire("give-prune",player)
	game.ReplicatedStorage:WaitForChild("Events"):WaitForChild("LogBindable"):Fire(player,"product One Life Stolen Prune Stick")
	return true
end

productFunctions[1671134878] = function(_product,player)
	game.ReplicatedStorage:WaitForChild("Events"):WaitForChild("LogBindable"):Fire("give-tempad",player)
	game.ReplicatedStorage:WaitForChild("Events"):WaitForChild("LogBindable"):Fire(player,"product One Life Stolen Tempad")
	return true
end

-- The core 'ProcessReceipt' callback function
local function processReceipt(receiptInfo)
	local userId = receiptInfo.PlayerId
	local productId = receiptInfo.ProductId

	local player = Players:GetPlayerByUserId(userId)
	if player then
		-- Gets the handler function associated with the developer product ID and attempts to run it
		local handler = productFunctions[productId]
		local success, result = pcall(handler, receiptInfo, player)
		if success then
			-- The user has received their items
			-- Returns "PurchaseGranted" to confirm the transaction
			return Enum.ProductPurchaseDecision.PurchaseGranted
		else
			warn("Failed to process receipt:", receiptInfo, result)
		end
	end
	
	print(receiptInfo)
	
	-- The user's items couldn't be awarded
	-- Returns "NotProcessedYet" and tries again next time the user joins the experience
	return Enum.ProductPurchaseDecision.NotProcessedYet
end

-- Set the callback; this can only be done once by one script on the server!
MarketplaceService.ProcessReceipt = processReceipt
1 Like

Just to set the record straight so nobody misunderstands this information, the serverside is NOT 100% safe from exploits. It just makes it more difficult for them because they can only simulate what a legitimate player is doing but using a script to do so instead of having to actually click buttons, move around, etc. Nothing related to process receipt other than I don’t want people thinking the serverside is safe from everything, because it’s not.

The only two things that are 100% safe from exploits are scripts in serverscriptservice and scripts in serverstorage that are not connected to any other scripts except for ones within those two locations. If they are connected to local scripts or other scripts outside of that location, they are still considered unsafe from exploits.

My two messages that I just sent should explain how they are making it so that the stuff runs. Unless they meet all of the conditions I mentioned, they are still vulnerable to exploits

The script is located parented to another script which is under the ServerScriptService so unless I need to parent it to ServerScriptService because why it should by all means not be accessible at all and thus my confusion starts, there are no remote events or bindable functions that trigger the thing it’s within its script using the process receipt as shown so unless they can now spoof the process receipt I am left at a complete confusion state.

Both are under serverscriptservice? Or one of them is a server script but it’s somewhere else? I agree I am in a confusion state too if these are all under that spot. Exploiters physically cannot obtain information from there and the idea of them “guessing” what is inside of there is just complete rubbish. They always find a way to obtain information from scripts that are not in a secure place, and if needed they can decrypt it from there