How to stop client strings breaking your data store saves
Data stores only accept valid UTF-8, so one bad string can stop a whole profile saving. How to check with utf8.len and rebuild client tables on the server.
What it looks like
A remote puts a string or table from the client into the player's saved data. The server checks its type and length, but not that a data store can save it. One bad value makes the whole save fail, and everything else in that profile stops saving with it.
remotes.SetPetName.OnServerEvent:Connect(function(player, name) if typeof(name) == "string" and #name <= 20 then profile.PetName = name end end)
How to detect it
- Follow every value from a remote argument into saved data, directly or through a module, including keys of tables.
- Roblox's security guide says data stores only accept valid UTF-8. A string checked only with
typeofand#can hold invalid bytes. Look forutf8.len(text)returning a number before the string is stored; it returnsnilfor invalid UTF-8. - Tables from the client must be rebuilt on the server, not stored as sent. Keys can be NaN or nil and values can be
Instances. Roblox's security guide saysInstancevalues fail to save and table indices can't be NaN or nil. - Roblox's data store guide says not to store
inf,-infornan, because they don't conform to JSON. Report non-finite numbers from the client as Numbers from the client without range checks. - Check what happens when a save fails. If the profile keeps playing and trading while it can't save, a failed save can be used to duplicate items.
- Size is Saved data with no size limit. This check is about values a data store refuses whatever their size.
What an exploiter could do
They send one string with invalid bytes, or a table with a NaN key, and their profile can no longer save. Roblox's security guide gives the trade case: they trade items away, the other player's save succeeds, their own save fails, and when they rejoin they still have the items. Repeated, that duplicates anything tradeable. Shared saves, such as a guild record or a plot layout others edit, can be broken for everyone in them.
How to fix it
- Check every string from the client with
utf8.lenand refuse it if the result isnil, as well as checking its length. - Never store a client table as sent. Build a new table on the server from checked keys and values.
- Keep the whole profile saveable at all times. If a save does fail, stop trades and gifts for that player until a save succeeds.
Vulnerable example
local ReplicatedStorage = game:GetService("ReplicatedStorage") local PlayerData = require(script.Parent.PlayerData) local maxLength = 30 ReplicatedStorage.Remotes.NameOutfit.OnServerEvent:Connect(function(player: Player, name: unknown) if typeof(name) ~= "string" or #name == 0 or #name > maxLength then return end local profile = PlayerData.get(player) if profile then profile.Outfit.Label = name end end)
Fixed example
local ReplicatedStorage = game:GetService("ReplicatedStorage") local PlayerData = require(script.Parent.PlayerData) local maxLength = 30 ReplicatedStorage.Remotes.NameOutfit.OnServerEvent:Connect(function(player: Player, name: unknown) if typeof(name) ~= "string" or #name == 0 or #name > maxLength then return end if utf8.len(name) == nil then return end local profile = PlayerData.get(player) if profile then profile.Outfit.Label = name end end)
Common false positives
- Strings the server picks, such as an item id from a server table chosen by index, are always valid.
- Text that is only shown, not saved, can't break a save. Judge it with Player text shown to others without filtering.
- Values matched against a fixed list, such as
table.find(allowed, value), are already valid.