Security Of Require()

If I have a script stored in Project Content, and I require() that from a client script, as shown in the snippets below. I want to be sure that a hacker cannot alter the script in Project Content via the client script (for example, changing the price of the product to zero).

-- Script stored in Project Content
local MyAPI= {}

function MyAPI.GetProductPrice(id)
    return 100
end

return MyAPI

Then in a script in client context, I require the above:

-- Client script
local propAPI = script:GetCustomProperty("API")  -- reference to the script above

-- Require API
local API = require(propAPI )

This is a tricky one, as there are different levels of "secure". Generally when dealing with code that executes on the client, all bets are off, and you have to assume that somebody could change the code that's going to run. When dealing with something like purchasing in-game items, you want the server to be the one that ultimately decides whether or not the player can do that, what it's going to cost, etc. It still makes sense to have the client do some initial validation, so it can provide feedback to the player without having to involve the server, but you don't want the client to be able to say, "Hey, I just bought 999 health potions even though I have 0 gold." The server should see that and say "No, I don't think so, cheater!"

Digging into some of the details on your example, a savvy hacker could absolutely change what their client thinks a product costs. There are various ways they might do this, like modifying the game definition as it's downloaded and before the client sees it, or changing values in memory. The details aren't so important so much as the fact that they control their computer, so if they try hard enough, they can do whatever they want. The important thing is that they can't change what the server thinks a product costs unless you do something like make an event that changes the product's price. The required script that executes on the server creates its own copy of MyAPI that is separate from the copy of MyAPI created on the client, so any changes the client might make would not affect the server.

Let's look at a hypothetical situation where a hacker is somehow able to get some code to run on the server for your game. Maybe they shared some community content that had a script hidden in it, and you incorporated that content into your game without realizing the script was there. As your code is currently written, that hacker's script could potentially find an asset reference to your script, call require(), and then make changes to MyAPI on the server, substituting their own GetProductPrice function that makes everything cost 0. There is a way to protect against this, using a Lua metatable and metamethods:

-- Script stored in Project Content
local MyAPI= {}

function MyAPI.GetProductPrice(id)
    return 100
end

return setmetatable({}, {
     __index = MyAPI,
     __newindex = function(table, key, value)
                    error("Attempt to modify read-only table")
                  end,
     __metatable = false
   });

What the heck is going on there? Rather than returning MyAPI directly, which anybody could then modify, the code above is returning an empty table (the {} passed as the first parameter to setmetatable), with a custom metatable set on it. Metatables are a Lua concept that controls how a table functions, through the use of one or more metamethods. In this case, we set an __index metamethod that is called any time somebody tries to look up a key that doesn't exist. (Since it's an empty table, this will be always.) We've given it MyAPI as a fallback table to look things up in. We've also set a __newindex metamethod that is called any time somebody tries to insert a new key into the table. (Again, since it's an empty table, this is always.) That function fails with an error, preventing anybody from being able to add new keys. Finally, the __metatable entry prevents anybody (including you!) from being able to call getmetatable or setmetatable on the table you're returning, so nobody can tamper with what you've put in place. It's important to note that this metatable only affects the empty table we're returning in place of MyAPI. MyAPI itself can still be modified by anything that has a direct reference to it, but that should only be your script.

Is all of this worth it? That's up to you. Even with something like this in place, persistent or clever hackers may be able to find some other vulnerability if they're able to sneak a script into your game. Or if you're careful enough, you can prevent them from being able to execute code on the server, and then this becomes unnecessary. This sort of thing can help keep you from accidentally modifying MyAPI from other scripts by accident, so there can be other benefits. I think it really comes down to personal preference and coding style.

1 Like

The Project Content area is somewhat unclear since it does not have the Client Context and Server Context as does the hierarchy. What is it's context?

What if only a Server Context script require() the API (the API being in the Project Content). Would that mean that the API is not downloaded to the client at all - since everything is server side?

Things in Project Content don't inherently have a context, it all depends on how they're used. If a script is placed inside a Client Context, then that instance of the script belongs to the client context, but another instance of the same script could also be placed in a Server Context.

If you call require() on a script from a server context, then the required script is also executed in a server context. Or if you require a script from a client context, the required script is executed in a client context. That may seem somewhat obvious to say, but this is one of those things that can be important to understand when testing in single-player preview mode, with all the quirks that it has. Even though single-player preview mode is both server and client inside the editor, we try to keep those contexts separate as much as possible, so this is the rare exception where a required script may actually be executed more than once: once when required from a server context, and again when required from a client context.

For your last question, if a script inside a Server Context references another script, and that other script is not referenced from anywhere outside of a Server Context, then yes, it will be stripped from the download that clients get. I say "references" instead of "requires" here, because Asset References are the key part of that functionality. If you call require() on a hard-coded script ID, we aren't able to count that as a reference, and so it won't be properly removed from the client download when you might expect it to be. Also, calling require() is not... required... for that stripping to occur, as long as an Asset Reference exists.

1 Like