Protecting a ModuleScript's internals from damage in live server?

In the context of a script builder where anyone can run anything, I’d like to protect a ModuleScript against outside scripts that may try to damage it. I’m worried about it because there are lesser-known Lua features that make script security a nightmare. For example, setfenv changes where functions look for global variables.

Are there any hidden Lua features I should worry about that can break a running module aside from what I already know?

I already take the following measures:

  • Localize all the variables needed by functions that can be seen outside the module, so they work as upvalues instead of globals, rendering setfenv moot. This includes localizing normal Lua functions like ipairs.
  • Use assert for parameter-checking.
  • Create a proxy object that has a metatable providing read-only access to the module’s main table. This also prevents outside scripts from masking functions with rawset, which would work on tables regardless of their metatables.
  • setfenv all the public functions to a blank table to prevent getting any reference to the ModuleScript instance and other potential damage
  • Run callbacks in pcalls to prevent them from getting the module’s environment via the call stack.

Here’s an example of what I would think is bullet-proof of a ModuleScript.

-- Localize stuff.
local assert = assert
local typeof = typeof
local workspace = workspace
local Instance = Instance

-- Create the ModuleScript library.
local mod = {}
function mod.Explode(part)
	assert(typeof(part) == "Instance" and part:IsA("BasePart") and part:IsDescendantOf(workspace))
	
	local explosion = Instance.new("Explosion")
	explosion.Position = part.Position
	explosion.DestroyJointRadiusPercent = 1
	explosion.Parent = workspace
end

-- Setfenv the public functions to a blank environment to protect this ModuleScript's environment.
local blank = {}
setfenv(mod.Explode, blank)

-- Create the read-only wrapper for the main table.
local proxy = newproxy(true)
local proxymeta = getmetatable(proxy)
function proxymeta.__newindex(t, k, v)
	error("This ModuleScript is readonly.")
end
proxymeta.__index = mod
proxymeta.__metatable = "Metatable is locked."

return proxy
5 Likes

First of all, is this being used on the server side? (Being loaded from a Script and not a LocalScript) If so, then make sure it’s in ServerScriptService, then it’s basically untouchable by the client.

And that’ll work even if an exploiter has the highest level of script context?

ServerScriptService/ServerStorage does not replicate anything to the client. A client cannot access anything in it.

I would recommend what I call a “reference lock.” Because only your script can access a local you can store an empty table in a local. Whenever a function is called in your protected Module you can force them to verify using that reference.

The only step after that is ensuring your module’s functions cannot be modified. You can use the newproxy function for this. It creates a new userdata. Userdatas are immune to rawset and their metatables can be locked. By supplying true as the first argument you can access the metatable using getmetatable.

local referenceLock = {}
local module = {}
function module:Foo()

end
function module:Bar()

end

local moduleSafe = newproxy(true)

local meta = getmetatable(moduleSafe)
meta.__index = module -- Set the indexer to the API
meta.__metatable = "The metatable is locked." -- Lock it

return moduleSafe -- Return the userdata protecting the module

In your other modules/scripts you can have them return similar userdata values. If your module needs to authorize one of these they can use something like __newinded or __call. In your main module you’d require the other module and pass the table reference to it. Again, make sure the other module can’t be modified and make sure it is required before user code is run.

1 Like

I’m aware of the fenv functions and have used them in old work, but not really an expert on them or metatables.

It seems to me the easiest approach is to parse the code as a string, and just look for setfenv and getfenv calls, and if they’re are inappropriate calls, don’t run the code.

2 Likes

Do you mean parse all of the code as a string – and if so, wouldn’t that take a while with a long ModuleScript?

You can require the module script with the ID of the module script.

1 Like

Unfortunately, closed-source is no longer a thing, as they were having safety concerns. Therefore it should work if the module was made public.

Not quite sure what exactly you mean by ID, but I assume it was the IDs of the scripts in the library.

I’m not actually sure what OP is trying to do. If its in the context of a script builder as I thought it was, then the code being submitted will be sent to the server as string.

Most properly constructed module scripts are no more than 100-200 lines. If this is a script builder, good practice goes out the door so it could be 10k lines long, so you’re right it’ll take a few seconds.

1 Like

There is a module by @evaera called ReplicatedValue which I think would be what you are looking for: Events/ReplicatedValue.lua at master · RoStrap/Events · GitHub

Why would a script builder (what is that lol) be so much longer?

Anything ran on the client side such as a LocalScript cannot access ServerScriptService, so you can safely put any vulnerable code there. Even if the client changes anything, no other players would be able to see what they’ve changed.

A script builder is a type of game!! Basically, you could join, and run any code you wanted to give yourself really cool weapons or whatever, and then play with other players.

2 Likes

There is no need to parse this code as a string. You can simply overwrite the getfenv/setfenv functions anyway. My method is meant to leave these unchanged and still protect code.

For others asking, a script builder is a game which allows you to test/show off your code to other players. The most well known script builder is Void SB and has existed for an extremely long time. Writing in proper script security is hard but there are a few ways like the way I showed which can prevent external scripts from accessing your code.

Metatables are basically a must have for script builders, otherwise you’ll have some very terrible security. TeleportService, LogService, etc will be available to people allowing them to do malicious things. You need proper moderation and security or you risk being punished.

2 Likes

So when I need to call a function from the ModuleScript, I can just do moduleSafe:Foo() and it’ll forward to module:Foo()?

Yep. __index controls when a table or userdata is indexed (meaning when a value is looked up). It can be a function or a table. __newindex works the same way but it controls when a new index is set (meaning when a value is set in the table).

The documentation for metatables on developer.roblox.com explains these and other metatables so I’d recommend reading there.

1 Like

For future readers that will be lazy like me, here’s a link to the metatables page.

Yes, though it’s a ModuleScript. Putting things into ServerScriptService exposes them to other scripts on the server.

You’re probably thinking about Experimental Mode or non-FilteringEnabled? That’s now completely gone. Clients can’t do much to any server now.


I’d like to write a ModuleScript that lets me send commands across servers in the same game, though not being able to keep the ModuleScript’s code secret prevents me from storing a secret MessagingService topic name inside of it. Though that’s not my main issue.

An external ModuleScript is loaded from the library in this way:

require(123456789)

Though I sometimes see ModuleScript loaders written like this:

require(123456789).Start()

Nah. I’m not making a script builder. That’s too much responsibility for me. Instead, I’m asking about making a bullet-proof ModuleScript that I use on someone else’s script builder.


I’m not looking to replicate Lua objects between the server and players. I’m looking to protect a server-side ModuleScript’s integrity against everything that can be thrown at it within Roblox’s normal Lua sandbox.

1 Like

A point to be aware of is that pcall(0) still returns the same thread environment as outside the pcall. Also, I wouldn’t rely on pcall’s current functionality of making clearing the environment stack, that is more of an implementation detail which may change at some point (hopefully with warning if at all).

Since you’ve messaged me about my sandbox, I should mention that I’d be worried if you used it in production code since I have not exhaustively tried to break it. I must admit though, I do not know of a better solution.

A module runs in its own environment until it returns, only globals affect it. This means that all variables including anything accessed through game, script, or workspace will introduce variability to your script and is dangerous. Once it returns, the return value is another way to interact with the module. Any events connected to or callbacks given while the module was executing fall under global environment interactions. The return value must be properly secured as you described so that if a table is returned, it cannot be altered without the module’s permission. In addition, the script that required the module needs a way to verify the identity of the module. We will assume that only strings, numbers, booleans, userdatas, nil, and functions will exist in our return value. Outputted values need to be check so that they do not reveal information they shouldn’t. All are immutable and unchangeable from the outside except via userdata metamethods, function arguments, and the function’s environment.

If a module uses only local values then a changing environment will never affect it: values cannot be read or set that will have an effect. The only way to gain access to these variables in lua is through the debug library which Roblox has removed for security reasons. This allows us to shrink our examination to only global variables and function / metamethod arguments.

Arguments, of types string, number, and boolean are always safe and just need to be parameter checked. Interactions with userdatas and tables needs to be done very carefully since comparisons, indexing, and other interactions may cause a metamethod to be invoked and may cause unexpected behavior. Calling functions type, getmetatable, and rawequal as well as using userdatas and tables as keys in tables will not invoke any metamethods and can be used to work with them. Indexing them is dangerous and may result in infinite loops, errors, asynchronous operations, or unexpected behavior, exactly like calling a function argument. Return values from metamethods and functions should be treated with the same care as arguments. All arguments type should be identified and if it is a number, string, nil, or boolean then its value should be inspected to be within semantic program bounds.

Unless you specifically need need to work with them, I’d not accept arguments or return values of the thread type.

It should be noted that calling wait or any other asynchronous function (they are again only access through the global variables or arguments) may result in the script never resuming because it gives up the ability to run and may be permanently stopped by another script or the game. The state must be in a good point to quit when a calling any callback or a yielding lua / roblox function. Finally if your script runs too long without pausing or returning, expect to be rudely interrupted at an unknown location.

If the module always returns without running too long, localizes all state while loading, secures its return value, provides its identity, verifies callers identity where needed, only interacts with arguments without invoking any argument metamethods or functions along with only using synchronous lua calls, and performs value checking then negative interactions with other scripts / modules is not possible. Calls to metamethods or functions through globals and arguments can be done safely by expecting the function never to return (errors, infinite loops, async waits and thread removal), treating return values like arguments, and censoring call parameters.

The game engine / scripts with higher permissions may still be able to harm the module. I’m fairly confident that this post addresses all security issues relating to modules besides those dealing with semantic of the module’s interactions. Those, commonly known as “bugs” may be the most difficult threat to handle. I recommend creating unit and system tests along with adopting a functional programming style.

2 Likes

I can safely assume those are always Instances and their descendants are always Instances. There are, however, some weird things that can happen to them. For example, I usually see top-level services get renamed to random things like “???”, leading me to use game:GetService for everything except Workspace.

I would consider an Instance and its descendants unsafe as soon as it’s been parented somewhere in the DataModel.


Can you clarify this for me? I thought that server scripts can safely assume “require(assetId)” returns the real thing.


I do get that task scheduler functions like wait are unreliable because poorly-written scripts usually spam “while wait() do” to the point of lagging every other thing that uses those functions. It’s another reason RunService is my go-to for time-sensitive things.

I don’t know how a script’s thread can be stopped by an outside script. I usually don’t do anything CPU-intensive, so I don’t need to worry about the game engine killing it. However, I did think of something else.

I keep forgetting about coroutine manipulation. An external script can take control of the thread one of my functions are running in, and then make the thread resume before whatever asynchronous thing my function is calling can finish, which may result in errors, unexpected return values, or other unexpected behavior.

I should automatically isolate my asynchonous functions into their own threads and crash outside calls if their threads are resumed early. Maybe running these calls through proxy BindableFunctions would be sufficient.

Here’s an example of what can happen:

local function test()
	-- Expecting numbers. Getting "LOL" instead.
	print(wait(4))
end
local thread = coroutine.create(test)
coroutine.resume(thread)
wait(2)
coroutine.resume(thread, "LOL")

Output:

LOL
02:03:09.317 - attempt to call a nil value

Edit:

Oh. That’s right, once I let a function yield, it may never complete, because other scripts have a chance to crash the server, or at least halt its Lua VM thread for a really long time.

Also, I realized there is a thing called “script context”, which depends on how threads are created and affects when the Roblox engine disconnects events and halts asynchronous functions. If a thread belonging to a regular script calls an asynchronous function from a ModuleScript, and then that script gets deleted before the call is complete, then the function will never continue.