Moved to GitHub
As of now, H6x’s development has officially moved completely to GitHub now.
I will no longer be updating this thread or posting updates, security information, documentation, etc here. I encourage you to post issues or feedback on the GitHub repository. I have also enabled the Discussion section of the repository, and will be happy to answer questions and offer assistance there. Feel free to shoot me a PM here as well!
I may later create a new post on the devforum when the project has developed further, but currently do not currently intend to.
Original post
Disclaimer / Downloads
Use H6x at your own peril! Remember that you are running untrusted code. While H6x makes security much easier to pull off and is designed to contain user code in-sandbox robustly, that doesn’t make it bullet proof to the values and functions you provide access to. Try not to give a sandbox more power than you strictly desire.
The marked solution includes the latest release and the download link.
H6x - Script Sandbox
Hello!
H6x is designed to be a fairly user-friendly, and secure sandboxing utility for executing untrusted code on the server. Note that on its own, while it does provide a few sandbox presets for various use cases, it isn’t a secure sandbox. It is only a tool to create and manage sandboxes & their environments in a secure form.
It provides interfaces for blacklisting, whitelisting, and remapping (redirecting) values by type, reference and more. It dynamically “sandboxifies” values through a value poisoning system. All values retrieved from a poisoned value go through all of the validations and changes you define on the sandbox, and are then themselves poisoned before being provided to user-space code.
Examples of blacklisting
Models, folders, scripts, and parts (but no require
)
sandbox:Blacklist(require) -- Blacklist require (Users can access ModuleScripts, but not require them)
sandbox:BlacklistClass("Instance") -- Blacklist all Instances
-- (Note on above: BlacklistType will not work as you may expect here, because it is processed before instance-specific exceptions. This may change in the future. Thus, the above must be used.)
-- Example of how to except certain classes
sandbox:ExceptClassName("Model") -- Allow explicitly Models (but, not things which extend Model)
sandbox:ExceptClass("Folder") -- Allow folders (and instances which extend them)
sandbox:ExceptClass("BasePart") -- Allow all kinds of parts
sandbox:ExceptClass("LuaSourceContainer") -- Allow all kinds of scripts
More restrictive example
sandbox:BlacklistClass("Instance") -- Blacklist all instances
sandbox:BlacklistType("function") -- Blacklist all functions (Including ones defined as globals inside of the sandbox)
-- Whitelist all of these particular functions (by reference)
sandbox:Except(print) -- Allow accessing print
sandbox:Except(warn) -- Allow accessing warn
sandbox:Except(error) -- Allow accessing error
sandbox:Except(debug.traceback) -- Allow accessing debug.traceback
Example of a redirect
sandbox:Redirect(print, myCustomPrint) -- Replace the print function with a custom one when it gets accessed inside the sandbox
If you want to, you can also create an empty environment to work with and insert values into it externally:
local environment = {
print = print,
error = error
}
-- This will eventually get its own feature, but, you can do it manually like so:
sandbox.BaseEnvironment = nil -- This is required, otherwise H6x.Environment.new overwrites it (Technically a bug)
sandbox.BaseEnvironment = H6x.Environment.new(sandbox, environment) -- Override the sandbox's base environment
-- Elsewhere you might assign values too:
environment.abc = 123
-- These values are not directly accessible if they are blacklisted. Your blacklists & redirects must allow them to be accessed,
Features
(Subject to change)
Blacklisting
In H3x, you defined custom hook handlers to decide what happens to values, when, and how. H6x has greatly simplified this into a blacklist system. More control will be added for more custom behaviours, but, currently blacklisting is the only way to do this.
You can blacklist types, specific values, specific ClassNames, and specific class types of instances.
Value “poisoning”
In H6x, values become “poisoned” inside of a sandbox. These poisoned values always yield other poisoned values when used in code, and, they are proxies to the unpoisoned (real) values.
H6x relies on the fact that a script can only access outside data using inside data. When a script is ran using H6x, a sub environment is created using the environment the H6x module runs in. The sub environment automatically poisons all values requested from it, which, means that a script should only ever be able to access poisoned values no matter what, even if you try to return an unpoisoned value to the sandboxed code.
Sandbox Imports & Exports
A lot of other sandboxes used a similar approach, but, sometimes they want to load an unsandboxed value inside of the sandbox, so, sometimes exceptions are made so that external code can do unsandboxed actions.
H6x uses a system of “imports” and “exports” instead. You can import functions into a sandbox to make them compatible with the sandbox so that all values given to your external code become unpoisoned and all values you give back become poisoned.
You can export functions from the sandbox to make them compatible with use in external code, so, when using them in external code, all values you pass in are poisoned, and all values you get out are unpoisoned.
Currently unsupported features/behaviours on the TODO list:
Currently, the following features are intended to be in later versions of H6x, but, currently do not exist:
-
getfenv
&setfenv
protections for exported functions (Basically, directly calling functions. This excludes functions executed in a runner). Currentlygetfenv
andsetfenv
can be used by malicious code to modify the environments of scripts outside of it. They can’t technically escape the sandbox, but, they can manipulate an explore your code this way and potentially gain access to H6x.
Documentation
Sandbox
The Sandbox
class is the only class you really need to use for most use cases. Some other classes may be useful or desirable in more technically involved use cases.
Sandbox
Sandbox Sandbox.new(dictionary options)
- Returns a brand new sandbox, optionally configured with the given options.
The options table (with the defaults) looks like this:
{
NoRestrictionDefaults = false, -- Whether or not to apply default restrictions automatically (Can be applied manually with Sandbox:RestrictionDefaults())
NoRedirectorDefaults = false -- Whether or not to apply default redirectors automatically (Can be applied manually with Sandbox:RedirectorDefaults())
}
function Sandbox:LoadFunction(function functionToLoad)
- Applies the sandbox environment to the given function & exports it, returning the exported copy of the function
function Sandbox:LoadString(string code)
- Loads a string of code using loadstring
and calls Sandbox:LoadFunction()
void Sandbox:Restrict(Variant value, Variant error, boolean terminate)
- (Similar to blacklisting) Restricts a given value with an error message (If unspecified the built in “No output from Lua.” message will occur instead of the given error message). Optionally terminates the whole sandbox if accessed. All primitive types which don’t use references are ignored. (E.g. thread
can be restricted but string
cannot)
void Sandbox:RestrictTree(table object, Variant error, boolean terminate)
- Restricts an entire table and tree of values. This is used to restrict the H6x and its sub functions from being accessed. Currently for the sandbox itself basic restriction is used since using this would restrict every value the sandbox has access to. In the future the Sandbox’s class will be restricted and specific properties of the Sandbox will be restricted.
void Sandbox:EnableActivityTracking()
- Currently just throws an error. In the future this will enable activity tracking.
void GenerateActivityReport(string format)
- Generates a report of a script’s full activity. Valid formats are data
, which returns a structured table (which can be manipulated by your code directly to change the internal activity history) or h6x
which returns a readable format similar to the one mentioned above. luac
is a planned format which will return vanilla lua bytecode (Not to be confused with “luac scripts” used in some old exploits)
Variant... Sandbox:ExecuteFunction(function functionToExecute)
- Loads the function into the sandbox and runs it in a Runner
script (Returns the unpoisoned results)
Variant... Sandbox:ExecuteString(string code)
- Identical to the above (Except it loads a string)
function|table|userdata Sandbox:Import(function|table|userdata value)
- Imports a value into the sandbox for use in the sandbox.
Imported tables are simply marked so their child values get imported. Imported functions take poisoned values, unpoison them, passes them to the original function, poisons the results, and finally returns them back to the sandbox.
function Sandbox:Export(function value)
- Exports a function retrieved from the sandbox for use outside of the sandbox. Currently tables cannot be exported automatically.
Exported functions take unpoisoned values, poisons them, passes them to the original function, unpoisons the results, and then returns them back to you. It’s not recommended that you provide sandboxed code with access to exported functions as they may provide it a way to partially access unpoisoned values.
Variant Sandbox:Poison(table|userdata|function object, int errorLevel, int restrictionErrorLevel)
- Takes an object and returns a poisoned copy of it for the given sandbox. If the object is a primitive its returned with no changes. Poisoned values should automatically poison anything accessed from them. (E.g. poisoned + unpoisoned
, poisoned.Index
, etc should all be poisoned values).
errorLevel
can be provided to result in an error when a blacklisted object is accessed, if this argument is not provided the object will be replaced with nil
. This level determines where in the stack tree the error happens (e.g. 1 = at the place you called it, 0 = at the root of the script)
void Sandbox:Redirect(Variant valueA, Variant valueB)
- Directly redirects any tracked references for valueA
to valueB
. valueA
must be non nil.
Note: Only one redirect or redirect handle can be set for valueA
, repeated calls will overwrite the redirect.
This also cannot globally redirect a value, meaning it can be detected by ran code if they have untracked access to valueA
since, its well, untracked. For example, if you redirect a string "abc"
to "cba"
the script can still use the string "abc"
as long as they don’t store and then access it in a tracked location (such as in a local variable)
In the future there may be support for tracking locals and upvalues for executed/loaded string code by injecting tracker code around each. For example, injecting this to wrap any local or upvalue:
-- Before injects
local someValue = "abc"
doSomething(someValue)
-- After injects (A randomized tracker function is added in the global environment that the script would have to brute force to access intentionally)
local someValue = __track1DB8915C795447CC90BA8C71E18B7CE5("abc")
doSomething(__track1DB8915C795447CC90BA8C71E18B7CE5(someValue))
void Sandbox:RedirectHandle(Variant valueA, function callback)
- Same as above, except, valueB
is derived from the callback function. The callback function is passed the sandbox as the first argument and whatever valueA
is as the second argument. (There is also internal behaviour to omit the sandbox as the first argument and call the callback in a “safe mode” but there is no way to utilize this yet. This is in case you want to utilize a function from a sandbox as the handle in a more performant way than wrapping the callback in a function)
void Sandbox:RemoveRedirect(Variant valueA)
- Removes the redirect or redirect handle for valueA
void SetScript(Script newScript)
- Sets the emulated script to the target script instance. (Currently this behaviour does not differ to redirecting the H6x module script to the target, but, in the future this may support things like syncing the .Disabled property with script termination)
void Sandbox:RedirectorDefaults()
- Applies default redirects (See options arg in Sandbox.new()
)
void Sandbox:RestrictionDefaults()
- Applies default redirects (See options arg in Sandbox.new()
)
void Sandbox:Blacklist(Variant value)
- Prevents the given value from ever being accessed from a poisoned value, always returning nil instead. (E.g. Sandbox:Blacklist(game)
prevents any reference to game
and instead replaces it with nil)
If a primitive value is provided it will be rejected.
void Sandbox:Except(Variant value)
- Creates an exception in blacklisting rules for the given value. This allows you to, for example, blacklist all types of Instance
, but allow sandboxed code to only access script
.
void Sandbox:BlacklistType(string typeName)
- Blacklists the given value type. For example, “Instance”, “Vector3”, “CFrame”, etc.
void Sandbox:BlacklistPrimitive(string primitiveType)
- Blacklists all primitives of this type (e.g. “string”). This isn’t very useful, but, it exists.
void Sandbox:BlacklistClassName(string className)
- Blacklists a specific ClassName. For example, blacklisting Model
but not WorldModel
(which extends Model)
void Sandbox:ExceptClassName(string className)
- Same as above, except, it creates an exception for that specific class
void Sandbox:BlacklistClass(string className)
- Blacklists the given class and any class that inherits from it. (E.g. BasePart
blacklists all part types)
void Sandbox:ExceptClass(string className)
- Same as above, except, it creates an exception for that class type
void Sandbox:Unblacklist(Variant valueOrType)
- Unblacklists a specific value reference or type name, or, if an exception was created for the specific value or type, clears it. Do not pass numerical values to this function if you blacklist primitives otherwise you’ll likely unblacklist different primitive types. (Primitive type are represented with an integer id)
void Sandbox:UnblacklistPrimitive(string primitiveType)
- Unblacklists a primitive type.
boolean Sandbox:InBlacklist(Variant value, int errorLevel, int restrictionErrorLevel)
- Checks if the given value would be blacklisted, or, optionally throws an error.
errorLevel
can be provided to result in an error when a blacklisted object is accessed, if this argument is not provided the object will be replaced with nil
. This level determines where in the stack tree the error happens (e.g. 1 = at the place you called it, 0 = at the root of the script)
Variant Sandbox:FilterValue(Variant value, int errorLevel, int restrictionErrorLevel)
- The same as the above, except, if the value is blacklisted, nil
is returned, otherwise, the value is returned. This is used by Sandbox:Poison()
internally.
void Sandbox:Terminate(string terminationMessage)
- Terminates the sandbox, disallowing any code from running within in (with the exception of basic for & while loops which are terminated automatically)
NOTE: This does not currently resume yielding code, so, threads can linger. Most threads will eventually be garbage collected (e.g. threads which permanently yield), but, it is possible for someone with malicious intentions to create a memory leak this way. This will be addressed in the future.
void Sandbox:CheckTermination()
- Throws the termination error if the sandbox was terminated.
void Sandbox:Unterminate()
- Unmarks the sandbox as being terminated. This does not resume any old code, that code would be dead, but, it does allow the sandbox to be reused.
Unfinished features
- The environment class has a JavaScript style property descriptor system (E.g. Object.defineProperty & Object.defineProperties) This hasn’t been fully fleshed out yet.
:get
,:set
,.value
,.writeable
, and.configurable
are all valid properties for a descriptor..configurable
and.writeable
are currently identical to eachother. For the getter and setter callbacksself
is the equivalent ofthis
from JavaScript (assuming you use the:
notation).
Other classes (Need documentation)
-
Environment
- A data structure for sub environments -
Runner
- A class which utilizes theRunner
module to execute code in isolation with a Sandbox. This basically gives the target code its own script, stack trace, and completely isolates its environment. -
Reflector
- A class mainly intended for internal use which creates a metatable that reflects metamethods from a poisoned object to an unpoisoned object. This is used by the poison system to create proxy objects (And will import the metamethods by default excluding __index and __newindex)