PROTECT YOUR REMOTES! (UTF8 + NaN exploits)

Introduction

You might be thinking what is this dude talking about? In general, there are vulnerabilities most developers don’t know about, and they aren’t really documented. Basically, it’s UTF-8 and has an Infinite number of vulnerabilities, which if exploited can cause hackers to have infinite currency in your game or rollback/corrupt their data which they can use for dupes and other data-related exploits.

(NOTE THAT I WROTE THIS PRETTY RUSHED SO PLEASE EXCUSE ANY SPELLING MISTAKES)


How do they work

UTF8 Unicode

Let’s start with the UTF-8 Unicode exploits, just to set a scene we have Billy and Timmy both have a game with pets and want to add renaming, but Billy uses proper practices against these Unicode Exploits, and Timmy doesn’t lets see what happens when a hacker figures this information out.

The remote event sent might be something like this:

{
["PetID"] = 0,
["Name"] = "Name"
}

For a normal request it’ll send the name in as a normal string, but an exploit might send a request like this:

{
["PetID"] = 0,
["Name"] = "\255\231"
}

Seems like a normal request right? Well, not those data are Unicode that Roblox will automatically translate, when I print that exact string I get:
image

This is pretty bad because it’ll hit the maximum data save size for Roblox and error and not save their data at all. (Scary right)

The above explanation could be exploited, if you cache the player data and save it at selected intervals and use the cached data for any edits and save on the player leaving/certain intervals (basically ProfileStore/ProfileService) so it won’t corrupt instantly until you save on a datastore. So the hacker could send a trade with the data with the malicious Unicode saved, and trade the pet they want to duplicate to an alt or such. Then they’ll leave to trigger that save which will corrupt their data rolling them back to before the trade meaning both parties have the same pet.

Billy’s Approach to fix this:

So let’s go back to Billy how Billy’s system works, is that it checks the string for any Unicode character above 127 bytes and if it is he sanitizes it from that Unicode and then saves the data. (Example Modules below), this works because if you detect the specific Unicode characters that’ll break Roblox, then you’re safe as long as you remove them before saving.

Timmy’s Approach to fix this:

Well since Timmy on the other hand doesn’t know how the dupe works, he might just disable trading from his game until he figures out, or maybe even indefinitely (which will lose him engagement), or maybe he’ll get scammed out of tons of Robux by exploiters telling him they’ll fix the exploit. This might go two ways, they fix it, and boom no more dupe but Timmy doesn’t know how it works still or they just take the money and scam him.


Infinite Numbers:

Let’s go onto how infinite number exploits work you have a system in which you buy Eggs or whatever for your game, but your game also sends the amount of eggs to purchase (which an explorer can put in a negative infinity to exploit).

The remote might look something like this:

{
["EggID"] = "BasicEgg",
["Amount"] = 3
}

A remote and exploiter might send in would be like this:

{
["EggID"] = "BasicEgg",
["Amount"] = -1/0
}

More examples of numbers exploiters might use:

0/0
inf
-inf
NaN (or any variant)
0/1
math.huge()

So why do these cause infinite numbers? Well most of the examples like -1/0 or 0/0 cause division errors, which Roblox takes in as infinite numbers (weird but whatever). The inf one is what Roblox uses for the math.huge() function if you ever tried to print it you’ll see that. And NaN means “Not A Number”, which is caused when errors with math (like the said division one) are inputted.

Why do these work? Think about it like this if you are doing multiplication to get the total cost of the eggs and a negative infinite is sent then your price is negative and it infinite, so when it checks the price it’ll 100% be less than or equal to the amount of money the exploiter no matter what if not dealt with properly. So it passed the price check now we gotta remove the price from their money, and guess what happens when you subtract a negative? It becomes positive so it’s like you running += math.huge() on the hacker’s currency. Boom they now have infinite money, all because they sent in a negative infinite value.

Billy’s Approach to fix this:

So our good friend Billy and his good practices might do one of two ways, he will either check if the amount is negative and make sure to use the absolute value, or he might want to detect the hacker so he’ll just do the same thing, but also use whatever his logging system to log negative values.

But what if Billy’s system needs negative values? Then Billy can just test for infinite values like this (modules sourced below) he can take the absolute value of the number, and test if it’s equal to math.huge()

Timmy’s Approach to fix this:

Well, Timmy over here might not know how all of this works, so what he might do is run a check on the amount of currency if its NaN or such when the player joins (hopefully not on the client), but let’s say Timmy the beginner scripter is doing that and sending a remote to the kick the player, well that sucks for Timmy because guess what his client-sided code will be bypassed by 90% of exploiters. They can either block the remote from firing, change the value they are checking on the client, or so many more bypasses (this is the reason you shouldn’t check for these types of stuff on the client.


Code to help you fix it!

(Please note I made these very quickly with little testing make sure to test before using them in your game production)

UTF8 Translator

UnicodeTranslator.rbxm (934 Bytes)

What this does is that it takes unicode and encodes them from “\255” into “/255” and so on so you can save it as a string, then be able to decode it into Unicode (great if you want to save names and want support for other languages/non-ASCII characters (like Chinese).

Source Code
local UnicodeTranslator = {}

function UnicodeTranslator.Encode(str)
	local encoded = str:gsub(".", function(c)
		local byte = c:byte()
		
		if byte >= 127 and byte <= 255 then
			return "/" .. byte
		else
			return c
		end
	end)
	return encoded
end

function UnicodeTranslator.Decode(str)
	local decoded = str:gsub("/(%d+)", function(num)
		local n = tonumber(num)
		
		if n and n >= 127 and n <= 255 then
			return string.char(n)
		else
			return "/" .. num
		end
	end)
	return decoded
end

return UnicodeTranslator


Assertions Module

Asserts.rbxm (1.1 KB)

This module helps you validate UTF8 and Infinite Numbers, basically for the number validation you input the number to validate, and it should return a bool if infinite or not (True for infinite, false for not)., for the String validation it first returns a Bool (true if Unicode over 126, or stuff that’ll break the data, false if not), and then the sanitized string (hopefully I didn’t test this part properly)

Source Code
local AssertionModule = {}

function AssertionModule.ValidateString(input: string): (boolean, string)
	if type(input) ~= "string" then
		return false, ""
	end

	if utf8.len(input) == nil or #input > 255 then
		return false, ""
	end

	local sanitized = {}
	for i = 1, #input do
		local byte = string.byte(input, i)
		if byte >= 32 and byte <= 126 then 
			table.insert(sanitized, string.char(byte))
		end
	end

	return true, table.concat(sanitized)
end

Function AssertionModule.ValidateNumber(input: number): (boolean)
	assert(type(input) ~= "number","Inputted value is not a number")

	if input ~= input or math.abs(input) == math.huge then
		return false
	end

	return true
end

return AssertionModule

EVEN WITHOUT THESE TECHNIQUES YOU SHOULD ALWAYS FILTER USER-GENERATED TEXT


made with :heart: by sfgij

17 Likes

TL;DR: Exploiters may make their own data fail to save, allowing them to duplicate items or currency. Don’t let exploiters corrupt their own data.

3 Likes

two points to add:
you can use utf8.len to check for invalid unicode (returns number if valid and nil if invalid)
trying to filter text with invalid unicode causes an error - and you should be filtering player submitted text anyways

3 Likes

I didn’t know the utf8.len thanks, and yeah I’ll add it

also nan is not equal to itself, meaning if x ~= x then x is likely nan

2 Likes

One check I always see missed is when dealing with tables you gotta validate the depth of the table eg { { {} } } has a depth of 3.

local function Deepify(Table, Original, Depth)
    Table = Table or {}
    Depth = Depth or 300
    Original = Original or Table

    if Depth == 0 then
        return Original
    end

    Table[1] = {}

    return Deepify(Table[1], Original, Depth - 1)
end
game:GetService("ReplicatedStorage"):WaitForChild("RemoteEvent"):FireServer(Deepify())
1 Like

Could you explain how this could be used?

Depending on whats done on the server side deep tables can cause infinite recursion, overflows, memory issues and performance issues. Take this table to string function

local function TableToString(Table, Indent)
	Indent = Indent or 0
	if type(Table) ~= "table" then
		return tostring(Table)
	end

	local Result = "{\n"
	for Key, Value in Table do
		Result ..= string.rep(" ", Indent + 2)
		Result ..= "[" .. tostring(Key) .. "] = " .. TableToString(Value, Indent + 2) .. ",\n"
	end
	Result ..= string.rep(" ", Indent) .. "}"
	
	return Result
end

game:GetService("ReplicatedStorage"):WaitForChild("RemoteEvent").OnServerEvent:Connect(function(player, data)
	if type(data) ~= "table" then
		return
	end

	print(TableToString(data))
end)

If the table is deeply nested, table to string func needs to recursively go through every key-value pair. Since each recursive call adds overhead, it can go to O(N²) in the worst case.

1 Like

can you explain more in depth on how this works?

Thanks for reporting the vulnerability, i was gratefull to add a ban return for this kind of values.

1 Like

I recommend not banning instantly and instead using ban waves. Makes it much more difficult to exploit without being detected

I think in my case banning straighforward is better, because they give up about a certain amount of time. Unlike banwaves, banwaves exploiters will use a alt and go back to they fun time.

Banning is a harsh punishment for somebody typing Nan or -1 into your system, just don’t let it go through… I feel comfortable saying that 90% of players who might try submitting those strings/ numbers will not be trying to actively exploit your game, and especially not in any meaningful way, i.e. through exploit engines. And users with access to those engines will most likely use those instead of wasting their time testing if a game accepts -1 or Nan values. Not to mention, players who accidentally type in -1, or want to name an item Nan legitimately.

Players can’t accidently press -1 since the selection system is properly scripted on the client side via a localscript, if they try to put something that would allow them to go to the end of the obby in an instant it would be harsh and unfair for others. Also that would mean they know about the RemoteFunction which would prove the point they are not innocent and trying to find security fails on the game.

I dont know your use case, but why is the user able to type in a number and teleport to that “stage” in the lobby? Even still, it may be a mistake. If your talking about a checkpoint system where the stage number comes from the player (either crossing a boundry or stepping on a part) and the server sets the current stage to that stage, you should look into moving that logic to the server.

I use both side logic, client will prevent non exploiters to get falsy banned and server will operate the teleport to the next stage. if the client try to put a stage he doesnt pass that will eventually mean he tryed to bypass the local script and is not innocent.

No worries, that was my intention to help people out

i would recommand the dev to use type(object sent by client) == "number then action, so it prevent further errors.

Wdym? I mean this post was made to warn people about and explain, With some simple fixes and stuff not really exactly perfect one size fits all fixes