Well, here we are. The age old addage of "Sanitize your inputs", strikes again... but with a twist!
A backstory
So to preface this, I've been developing for a game called Islands on Roblox for the past couple of years. It's a semi-popular game that's a sort of Roblox inspired variant on the Skyblock sort of mode for Minecraft.
It also has an economy, which of course brings it's own set of issues when a certain subset of your userbase... want to make real money off of these items.
Cursed user input
Firstly, I want to warn anyone who decides to add something like naming a pet, or writing text on a sign (for example) - Roblox has this problem where if someone puts some invalid UTF-8 text - as an example say they put "�" (where � is the invalid UTF-8 text) via exploits - Roblox will horribly fail to save it. This is also true for any types that aren't valid to be serialized in DataStores such as Vector3
s - so make sure you're also checking the type!
If somehow a user totally "legitimately" sends an invalid character in place of a string, it can cause the data to not save in general.
What could go wrong?
Lets start with a very simplified example that isn't Islands, but should get the point across. We're using ProfileService
to simplify this a bit, and because it's an example of how data is usually "handled" in Roblox games, and how it handles the data can lead to the game not saving data at all while the player's still in the game with the aformentioned invalid input.
Setting up our datastore code first:
local ServerScriptService = game:GetService("ServerScriptService")
local Players = game:GetService("Players")
local ProfileService = require(ServerScriptService.ProfileService)
type InventorySchema = { itemName: string, amount: number }
type ProfileData = {
coins: number,
inventory: { InventorySchema },
petName: string,
}
local defaultData: ProfileData = {
coins = 0,
inventory = {},
petName = "Pet Name",
}
local ProfileStore = ProfileService.GetProfileStore(
"PlayerData",
defaultData
)
local PlayerData: { [Player]: ProfileData } = {}
local function PlayerAdded(player)
-- Add the player profile to the `PlayerData` here - see https://madstudioroblox.github.io/ProfileService/tutorial/basic_usage/
-- ...
end
for _, player in ipairs(Players:GetPlayers()) do
task.spawn(PlayerAdded, player)
end
Players.PlayerAdded:Connect(PlayerAdded)
Players.PlayerRemoving:Connect(function(player)
local profile = Profiles[player]
if profile ~= nil then
profile:Release()
end
end)
function Player:GetData(player)
return PlayerData[player].Data
end
return PlayerData
Now that we have that, writing our code for setting the name of the pet and then looking at the code (ignoring the obvious bad simplistic design choices, which I've done to get to the point) - what are we missing here?
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlayerData = require(script.Parent.PlayerData)
local setPetName = ReplicatedStorage.SetPetName
setPetName.OnServerEvent:Connect(function(player, petName)
-- Guard clause to ensure the petName is actually a string
if typeof(petName) ~= "string" then
return
end
-- Guard clause for 3 - 64 character long pet names
if #petName > 64 or #petName < 3 then
-- Display an error to the user
return
end
local playerProfile = PlayerData:GetData(player) -- get the player's data
playerProfile.petName = petName -- set the pet's name
-- update the pet in the game etc.
end)
This checks that:
petName
is a stringpetName
is between 3 and 64 characters
This is fine usually, and otherwise would not be a problem. But lets add an extra layer of complexity where this becomes a problem.
- In their house they can store items
- Their inventory can also store items...
- These items are valuable and rare.
- The house is obviously separate from the player in your datastores due to being different data sets.
Now there's an incentive for bad actors to duplicate items in your game, to sell for real money.
All it takes is the above attack I mentioned, and now your player's inventory isn't saving. They can then place the items in their house - and rejoin the game - OOPS! now the item is back in their inventory, and also in their house! YOWCH! Now they rinse, repeat... and you cry in a corner contemplating your life.
Do also note that if you're only using one datastore, this could still be abused if your game has a trading system. Any item transferral can be abused in this way.
How do we fix this?
There's a nice little library for Lua, which you can use for Roblox. If you have anything that accepts user input and will save it into Roblox's Datastores - use this on each string value you're saving - reject anything that isn't valid.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local utf8_validator = require(ReplicatedStorage.utf8_validator)
local setPetName = ReplicatedStorage.SetPetName
setPetName.OnServerEvent:Connect(function(player, petName)
-- Guard clause to ensure the petName is actually a string
if typeof(petName) ~= "string" then
return
end
-- Guard clause to ensure petName is valid utf8
if not utf8_validator(petName) then
-- Display an error to the user
return
end
-- Guard clause for 3 - 64 character long pet names
if #petName > 64 or #petName < 3 then
-- Display an error to the user
return
end
local playerProfile = PlayerData:GetData(player) -- get the player's data
playerProfile.petName = petName -- set the pet's name
-- update the pet in the game etc.
end)
and now with this added protection, we've protected our pet from being abused to stop data saving. Obviously there's more that can be done here to prevent dupes, but the moral of the story is - MAKE SURE TO SANITIZE YOUR INPUTS! - who knew such a small thing can cause such big problems?