H6x - Secure Lua Sandboxing for User Code (Early Prototype)

Disclaimer

This is a VERY early prototype of H6x. Because of this it’s full of security issues (a few of which are mentioned in the unsupported features/TODO), bugs, performance issues, and more.

It’s pretty messy, so, I don’t recommend using it yet, but, if you’re interested, I really encourage you to play around with its features and provide feedback! That’s why I am releasing this prototype, after all, the more feedback, the better. Any concerns, suggestions, or issues you have are greatly appreciated.

H6x - Script Sandbox

Hello! :smile: I previously created this older script sandboxing tool called H3x which had a lot of issues.

H6x is intended to be a friendlier remake of that tool, and, should feature a lot of nice utilities for games which want to run user code. H6x will be less focused on being a tool for “true to Roblox” sandboxing, and more focused on being a tool for user code.

This tool has a few requirements for me, mainly, I want an easy way to utilize people’s sandboxed code outside of the sandboxed environment, and, I want an easy way for people’s sandboxed code to utilize unsandboxed code without being able to accidentally introduce sandbox escapes.


Prototype

After quite a few months here, I finally have what I consider to be a stable enough prototype for release. This is somewhere around my 15th unique attempt, all previous attempts had fatal flaws in their sandboxing, bugs, or were too difficult to use.

You may have originally seen me write about a prototype of H6x close to the end of 2020 on the original H3x thread. The prototype mentioned then is not this one, in fact, I have gone through several prototypes since that point. I decided to hold off on releasing anything until I was fairly confident with something.

Download

Use at your own peril!
The marked solution includes the latest release (This is to make it a little easier to update and less easy for you to accidentally download an old version)

Recommendations & examples for blacklisting:

This is an example of how I would recommend you implement blacklists. You should blacklist most things under a blanket, and, only allow what makes sense to allow for whatever you are sandboxing for.

sandbox:Blacklist(require)
sandbox:BlacklistClass("Instance") -- Due to the order blacklists are checked, BlacklistType will prevent you from excepting classes

-- Example of how to except certain classes
sandbox:ExceptClassName("Model") -- Only Model classes, but, not things like Workspace which extend Model
sandbox:ExceptClass("Folder")
sandbox:ExceptClass("BasePart")
sandbox:ExceptClass("LuaSourceContainer")

You can even do this, for example, though, this isn’t ideal since it can break their scripts:

sandbox:BlacklistClass("Instance") -- Blacklist all instances
sandbox:BlacklistType("function") -- Blacklist all functions (Even their own except when used only as locals/upvalues)

sandbox:Except(print)
sandbox:Except(warn)
sandbox:Except(error)
sandbox:Except(debug.traceback)

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 -- Currently this is required, otherwise the old environment gets applied to a few things internally by the environment class (Technically a bug, but, its just a remnant of some previous changes)
sandbox.BaseEnvironment = H6x.Environment.new(sandbox, environment) -- Override the sandbox's base environment

-- Elsewhere you might assign values too:
environment.abc = 123

Features in the prototype

This prototype version of H6x contains a few core features, and, will likely differ greatly from later versions of H6x. Don’t rely on APIs remaining consistent.

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 idea 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:

  1. pairs & ipairs for poisoned values. Currently the table will appear empty (since its just a proxy to the original). This will be solved by replacing pairs and ipairs with custom versions which will iterate over the unpoisoned version of the table and result in poisoned values and keys. (This was fixed due to a mechanism change, all functions initially in the global environment will work as expected. By default there is an explicit fix enabled in case this mechanism change is reverted in the future)
  2. Direct value redirection (E.g. directly redirecting any references for workspace.Baseplate to workspace.OtherBaseplate). This will be utilized by the above, and, will also make it easy to override specific functions just like you can blacklist specific functions (e.g. you might override Instance.GetChildren with a custom version. Since all instances reference the same function this would work for all instances) Values can now be redirected directly, or via a handler function.
  3. Activity tracking & name data (This should realistically come next update):
local RunService = game:GetService("RunService")

RunService.Heartbeat:Connect(function()
    -- Stuff
end)

Would be able to generate a data structure for lua code and/or something readable looking somewhat similar to the following pseudo example (Producing vanilla lua bytecode is also planned but probably won’t be implemented at the same time):

get game
get <Previous>.GetService
call <Previous>(<Previous-1>, "RunService") @ game:GetService("RunService")
get <Instance RunService "Run Service">.Heartbeat
get <Previous>.Connect
call <Previous>(<Previous-1>, <function 0xffffffff>) @ <Instance RunService "Run Service">.Heartbeat:Connect(<function 0xffffffff>)
  1. getfenv & setfenv protections. Currently, for exported functions (excluding functions executed in a runner) getfenv and setfenv 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.
  2. Protections against sandboxed code accessing H6x functions. Currently sandboxed code can access H6x if you allow it to, and, this is bad because it would allow the sandboxed code to manipulate its blacklist, or, change settings, effectively removing sandbox restrictions from itself. This will be solved by marking any H6x values and instantly terminating when a sandboxed piece of code attempts to access them. (If a H6x function or table, or the sandbox itself gets poisoned, the entire sandbox shuts down immediately) This is introduced via the Restriction features.
  3. script variable emulation. Currently, the script variable will be H6x’s module, because, this is the script the environment is extended from. If H6x’s module isn’t blacklisted, a piece of code can call require(script) to access H6x. This is introduced as the Sandbox:SetScript() function, and, currently is just a value redirect under the hood but in the future may get support for .Disabled and other things.
  4. (This is likely fixed by the same mechanism change that fixed ipairs/pairs, but, I didn’t implement a test module so I won’t guarantee it yet) require for ModuleScripts. Currently, the require function isn’t automatically imported. This means that sandboxed code cannot require module scripts since the require function receives a poisoned copy of the module. It can, however, require modules by their id (e.g. require(12345)).
  5. In-Script Runner. Rather than using a ModuleScript for the runner, a regular Script would be used. This would allow Termination via the .Disabled property which is much more proper and would fix a lot of things with sub threads and event connections beyond what has been newly introduced.

Documentation

Sandbox

The Sandbox class is the only class you really need to use. The other classes in H6x are used by the Sandbox class and the Sandbox class currently exposes better ways to interact with those classes than they do.

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 callbacks self is the equivalent of this from JavaScript (assuming you use the : notation).

Other classes (Need documentation)

  • Environment - A data structure for sub environments
  • Runner - A class which utilizes the Runner 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)

Latest update

35 Likes

Legacy downloads

H6x Prototype 5-2-21.rbxm (15.1 KB)
H6x Prototype 4-12-21.rbxm (11.9 KB)

Status update

! REPORTING SECURITY ISSUES !

If you find a security vulnerability, please report it immediately over PMs! Security is a big concern of mine, so, finding these issues is very important. If you find a security issue it will get a test module crediting you for the find and if its a big enough concern I may discuss giving a decent Robux bounty.

Give me your feedback!

If there is anything that the current form of H6x lacks that you’d really want to see, ask. I want H6x to be easy to use and secure and most importantly I want H6x to be applicable to any game that’s already using a script sandbox as well as to future games that just want to run generic user code.

I want H6x to be acceptable not just for script builders and learning resources but also just generic games. The reason H6x exists in the first place is because I want to create something to utilize in my own game as an actual gameplay component. Almost no script sandboxing tools exist on Roblox and none that I’ve seen would generally be considered fit for a full fledged game, and, I would like to change that.

So, I really encourage you to make suggestions and be critical of H6x! All concerns are important to me.

May 3rd Prototype Update

  • Added lots of new tests and improved a few old tests, including a few bugged ones. Everything that’s tested against is verified to some degree.
  • Slightly changed some of the mechanisms behind how values are poisoned. This should improve security a little in some backup cases and should fully fix ipairs/pairs for poisoned values. By default a backup fix is applied anyways in case any of those mechanism changes are reverted.
  • Added value redirection. Used to replace one value with another.
  • Added value restriction. Used to semi-securely eliminate values from the sandbox’s access. Can be used to automatically terminate a script which references a restricted value or to error out the thread utilizing it. (This stops sandboxed code from accessing the H6x module by default and partially stops sandboxed code from accessing its own sandbox variable if either are somehow exposed. The punishment is a full termination of the sandboxed code)
  • Fixed errors resulting in error in error handling instead of An error occured: No output from Lua. when erroring with a non string or non numerical value. This fixes a case which could potentially allow a script to detect its running in a sandboxed environment. (Currently there are probably a lot more cases scripts can detect this, so, if you notice one, let me know)
  • Fixed a bug with Reflector which caused the sandbox environment (and other reflected values) to incorrectly compare as not equal with the real value of the reflector

Plans (In the short term)

  • Add activity tracking and a way to generate readable and processable activity reports in different formats for different use cases.
    • Example formats: Generic lua code, lua byte code, a custom readable format, etc
  • In-Script runner
  • Guaranteed support for require for ModuleScript instances

Planned release date

I plan and expect to have something I consider acceptable for most games before May 31st. In reality I expect that H6x will be ready in a week or two but that’s not at all a guarantee as I have a lot of things on my radar.

Download

H6x Prototype 5-3-21.rbxm (18.9 KB)

9 Likes

Status update

! REPORTING SECURITY ISSUES !

If you find a security vulnerability, please report it immediately over PMs! Security is a big concern of mine, so, finding these issues is very important. If you find a security issue it will get a test module crediting you for the find and if its a big enough concern I may discuss giving a decent Robux bounty.

May 3rd Prototype Update B (Pre-Release)

  • Script emulation and In-Script Runners. H6x will automatically create a server or local runner script depending on what environment its running in, so, it can be used on the client or server. (With the exception of string based execution since loadstring isn’t available on the client) Runners can still be manually created as a module if passed true in their constructor.
  • Termination now uses the Disabled property of the runner. This results in much more reliable termination. If you want to emulate a module script, you should use Sandbox:SetScript(emulatedModule) rather than manually creating a module runner since its more reliable to use the script.
  • Test logs can now be suppressed and additional arguments can be passed to tests (may be used to support special options for different environments)
  • Added a fast mode for tests. Currently this just skips the Termination test which uses long wait calls to confidently make sure a script terminated in time. There isn’t a great way to do this check without long waits yet and its too slow for doing quick security checks.
  • Reduced the wait time for the Termination check so tests run a little faster.
  • Added a security check feature. It runs all tests in fast mode and throws an error if any of them fail. This would indicate that something isn’t quite right with how H6x is functioning, and, that could be due to a Roblox bug or an H6x bug. You should use pcall when requiring H6x and, depending on how sensitive an attack would be for your game, you probably want to disable features in your game that rely on it. You can ignore the security check but its not recommended.

Documentation changes


void Sandbox:SetScript(Variant script) - Will redirect the script variable to the target value (Doesn’t have to be a script or an Instance). Also tracks the Disabled property if the passed value is a BaseScript instance, disabling or enabling the emulated script variable terminates or unterminates the sandbox.


Doing the security check (Recommended)

To properly handle the security check, all you have to do is just wrap the require result in a pcall. If the security error happens you’ll still get access to H6x but its not recommended that you utilize it for user code if your game could be impacted.

If the security check fails, the module is thrown as the error which is why you get it back from the pcall. The reason for the use of a pcall rather than a regular call is that in the future the error may be invoked indirectly inside of an H6x feature.

local isSafeToUse, H6x = pcall(require(pathToH6xModule))

if not isSafeToUse then
	-- Disable your H6x features if it might impact your game
end

local sandbox = H6x.Sandbox.new()

-- Some code utilizing H6x

Ignoring H6x security checks

To ignore the security check just don’t pcall. The H6x module will work as expected.

local H6x = require(pathToH6xModule)
local sandbox = H6x.Sandbox.new()

-- Some code utilizing H6x

Updated short term plans

  • Add activity tracking and a way to generate readable and processable activity reports in different formats for different use cases.
    • Example formats: Generic lua code, lua byte code, a custom readable format, etc
  • Guaranteed support for require for ModuleScript instances (This should be resolved already it just needs some tests)
  • The ability to redirect require calls in sandboxed code, and, a utility for targeting redirects externally. (For example, if you are emulating a module and you want that module script to actually return the result from the sandboxed code when required in sandboxed, or, if you want to do so in external code, providing a replacement require function)
    • This is primarily useful if you’re loading an asset and can read the source code, or if you are creating a modding system.
    • It should also allow you to redirect non instances and non numbers too, for example, if you wanted to emulate pure lua code which uses require("somemodule") you could redirect that as well.
    • Additionally, sandboxes should be able to disable numeric asset id requires.
  • Add some “presets” for different sandboxes. For example, H6x.Sandbox.Empty.new() might create a sandbox with no access to anything and all restrictions in place by default (e.g. numerical requires). H6x.Sandbox.Basic.new() might create a sandbox that only has access to the basic lua libraries and doesn’t have access to instances and has all restrictions in place by default.

Download

H6x Prototype 5-3-21 B.rbxm (22.1 KB)

4 Likes

what exactly is this i am super duper confused rn

Lua sandboxing.
Sandboxing is like having permissions of what you can do and what you can’t do.

then why is he/she recreating tht in luau…?

So you can create a custom User Code environment. A bit of an advanced application.

It’s a sandbox tool, so, basically you can stop whatever code you run in it from doing certain things you don’t want it to do. That’s useful if you want to, for example, stop code from accessing sensitive services, like TeleportService or MarketplaceService in a script builder, or if you want to stop scripts from requiring outside assets.

For example, this is actually one reason I have been developing H6x, in my game I want to be able to run people’s code so they can create controllers that they can put on things to control them with code.

The reason I’ve written it in luau is because its designed for Roblox specifically. It uses loadstring to load code so loading strings of code on the client isn’t possible but you can on the server by enabling that on ServerScriptService.

3 Likes

Legacy downloads

H6x Prototype 5-3-21 B.rbxm (22.1 KB)
H6x Prototype 5-3-21 A.rbxm (18.9 KB)
H6x Prototype 5-2-21.rbxm (15.1 KB)
H6x Prototype 4-12-21.rbxm (11.9 KB)

Documentation additions


void Sandbox:RestrictAssetRequires() - The default. Disables numerical (asset) requires when in roblox mode. The error thrown is the same as when inputting invalid arguments to require normally.


void Sandbox:AllowAssetRequires() - MUST be called to enable requires in a sandbox, they will not be enabled by default. Enables numerical (asset) requires when in roblox mode.


void Sandbox:AddModule(Variant target, Variant module) - Maps the target input to an imported copy of module in all modes except disabled. (E.g. sandbox:AddModule("util", util) will map require("util") to an imported copy of util)


void Sandbox:SetRequireMode(string|Variant mode) - Sets the require mode. Valid modes are roblox (Default), vanilla (Mimics vanilla lua requires), disabled


Status update

! REPORTING SECURITY ISSUES !

If you find a security vulnerability, please report it immediately over PMs! Security is a big concern of mine, so, finding these issues is very important. If you find a security issue it will get a test module crediting you for the find and if its a big enough concern I may discuss giving a decent Robux bounty.

May 5th Prototype Update

  • !!! Fixed a big bug where non instance Roblox types were failing (E.g. RBXScriptConnection). The fix is done by defining types the user can manipulate (userdata and table). That means any Roblox type (RBXScriptConnection, RBXScriptSignal, Vector2, etc) will now work as intended with function calls.
  • Activity tracking! This isn’t finished and is still in an early form so there’s some stuff that’s a bit weird around it. Currently you can use two formats: h6x and data (default). data returns a list of history entries, the current valid types are get, set, getGlobal, setGlobal, and call. In the future getGlobal and setGlobal will be merged into get/set and at the moment there are duplicate get and set events for globals which show the environment tables.
  • require is now gauranteed to work for module scripts with Redirector Defaults on
  • Custom requires aka require redirects UNLESS the Redirector Defaults are disabled
    (Completely disables requires), custom (Uses a custom function, not really useful to change to this mode manually unless you’re switching modes at runtime), or you can pass a non string hook which will set the custom require hook and set the mode to custom.
    • See Documentation additions for details.
    • When using a custom require hook you can return multiple values, and, the results you return are NOT cached unless you do that yourself. The require hook can be anything that can be called and can throw errors if you want, it doesn’t have to be a function.
    • The disabled mode throws with “Require is disabled.” when require is called at all, no custom require hooks or module additions are used, this completely disables require.
  • The default termination message is no longer “Script terminated” instead, the value true is used which tries to do a silent termination by permanently yielding threads
  • Fixed an unhandled error when terminating inside of a metamethod (attempt to yield across metamethod/C-call boundary)
  • When using silent terminations and a script resumes itself, 100 attempts to yield it are done before falling back to using an error
  • The fall back for silent termination failures is to emit a cannot resume dead coroutine error
  • When a tracked function call is done, termination checks are now done before AND after the call, previously the check was just done before which allowed the script to still potentially utilize call results (e.g. if a yield happened)
  • wait is now overwritten by Redirector Defaults with custom behaviour (To avoid weird errors with terminations). This custom behaviour uses spawn which internally has a very similar behaviour to wait and passes the same arguments that wait returns to its callback. Only if the script isn’t terninated when the delay is up is the thread resumed and the callback arguments returned.
  • Furthermore, delay itself has also been overwritten and so has coroutine.resume with the same check, the callback isn’t called if the sandbox is terminated and the thread won’t be resumed if the sandbox is terminated.
  • Further improved Termination check, there is no longer an attempt to call nil value error outputted to the console

Test activity report:

getGlobal print = <function print>
get <table table: 0x96a96acdad145313>.print = <function print>
call <function print>("[H6x]", "Runner initialized.", "\
", "	Environment correctly applied:", "YES.", "\
", "	Environment:", <table table: 0x96a96acdad145313>, "\
", "	Sandboxed:", "YES.", "\
", "	Sandbox:", <table table: 0x94e2724f1eaaacd3>) 
	return 
getGlobal game = <Instance game "H6x Script Sandbox">
get <table table: 0x96a96acdad145313>.game = <Instance game "H6x Script Sandbox">
call <function GetService>(<Instance game "H6x Script Sandbox">, "RunService") 
	return <Instance Run Service "Run Service">
call <function import>(<Instance game "H6x Script Sandbox">, "RunService") 
	return <Instance Run Service "Run Service">
get <Instance Run Service "Run Service">.Heartbeat = <RBXScriptSignal Heartbeat>
call <function Connect>(<RBXScriptSignal Heartbeat>, <function function: 0x898b0a1017f52553>) 
	return <RBXScriptConnection Connection>
call <function import>(<RBXScriptSignal Heartbeat>, <function function: 0x898b0a1017f52553>) 
	return <RBXScriptConnection Connection>
set <table table: 0x96a96acdad145313>.connection = <RBXScriptConnection connection>
setGlobal connection = <RBXScriptConnection connection>

Plans

  • Add prebuilt sandboxes. For example, H6x.Sandbox.Empty.new() might create a sandbox blacklisting everything for you to adjust yourself (a “whitelist” mode in a sense). H6x.Sandbox.User.new() might create a sandbox tuned to running Roblox user code that doesn’t have access to the DataModel or any instances. H6x.Sandbox.Roblox.new() might create a sandbox that has access to the regular Roblox environment. H6x.Sandbox.Vanilla.new() might create sandbox with vanilla lua compatability features. H6x.Sandbox.Plugin.new() might create a sandbox with plugin compatability features (e.g. for use on the client).
  • Improve h6x format for activity reporting (make simplified names like in the original example) and fix some issues around it. Also, marking internal calls so they can be optionally hidden from the report would be good to do.
  • Add lua and maybe luac (vanilla lua bytecode) formats
  • Add feature for blacklisting instances linked to the DataModel. This would allow for code to create instances and use them or access instances you give to it if they’re in nil but not if they could access game or anything under it.
  • Add feature for
  • Add luau typing for H6x classes

Download

DOES NOT INCLUDE TESTS FOR THE NEW FEATURES YET. They’ll be added once I actually finish them (probably in a few hours). If I need to do bug fixes I’ll post a new status update with them included. I wanted to get this update out despite being a bit short on time tonight.
H6x Prototype 5-5-21 (Missing update tests).rbxm (26.6 KB)

2 Likes

I’m probably going to have to include tests later today, I didn’t end up getting to write them so far, so, if any stuff is not working when you’re using it let me know.

There shouldn’t be any security issues with the require features themselves since they’re basically relying on already existing features in the Sandbox class so I’m not too concerned personally.

On the other hand, the activity tracking can cause errors that break code if there is some weird specific edge case I missed in one of the tracked events and I can’t easily write tests for that to check all edge cases so its a bit iffy. There’s also no way to disable it yet despite that being a plan.

Also, if anyone has specific things they think should be tracked besides calls and indexing I’d love to hear it because off the top of my head I can’t think of much that would go under tracking. It could be specific, at some point I’m going to have a feature which simplifies patterns of events, so, you’ll get things like method calls.

I’m going to probably add an obfuscation mode too which seeks to getfenv calls and tries to back track to wherever the start of the code is most likely to be. Some obfuscators do a rediculous amount of calls and that completely floods the history. Most of the garbage that a lot of obfuscators intentionally add gets completely filtered since it goes untracked, because, its not actually used, which, is super useful.

So far I’ve been able to use the activity tracking to pretty well understand some random obfuscated bits of code, which, is great, that’s one of the main use cases.

I’d love to see what the caveats and benefits of of tracking some obfuscators are because it would give a pretty good idea of how to improve the activity tracking for better understanding obfuscated code.

5 Likes

Confirmed security vulnerability in prior versions!

This vulnerability fails to poison or filter certain values going into or out of the sandbox in a specific but common case. This does not lead to any sort of sandbox escape directly but may allow someone with malicious intentions to access things they’re not supposed to be able to access, potentially including instances linked to the DataModel assuming some criteria are met.

To avoid revealing the details for now in case anyone happens to be using H6x in a fragile environment, I’m not going to explain what the exact issue is. It’s pretty simple, but potentially dangerous and common in specific scenarios.

Legacy downloads

H6x Prototype 5-5-21 (Missing update tests).rbxm (26.6 KB)
H6x Prototype 5-3-21 B.rbxm (22.1 KB)
H6x Prototype 5-3-21 A.rbxm (18.9 KB)
H6x Prototype 5-2-21.rbxm (15.1 KB)
H6x Prototype 4-12-21.rbxm (11.9 KB)

Documentation additions


Sandbox Sandbox.<PRESET>.new(table options) - Creates a sandboxed based on the preset (See changelogs below, will add full documentation when I merge into the main post)


void Sandbox:BlacklistTree(Instance rootInstance) - Blacklists an instance and its descendants


void Sandbox:ExceptTree(Instance rootInstance) - Excepts an instance and its descendants


void Sandbox:UnblacklistTree(Instance rootInstance) - Unblacklists an instance and its descendants


void Sandbox:UnblacklistClass(string className) - Unblacklists a class (See Sandbox:BlacklistClass())


void Sandbox:UnblacklistClassName(string className) - Unblacklists a specific class (See Sandbox:BlacklistClassName())


Status update

! REPORTING SECURITY ISSUES !

If you find a security vulnerability, please report it immediately over PMs! Security is a big concern of mine, so, finding these issues is very important. If you find a security issue it will get a test module crediting you for the find and if its a big enough concern I may discuss giving a decent Robux bounty.

May 28th Prototype Update

If you didn’t already notice or are just checking out this project unfortunately I’ve been very short on time. Most of this update was done just after my previous post (including these changelogs) but unfortunately I just didn’t have the time to really do too much else, and that’s likely how it will be for a while.

If I get any suggestions or reports I will try to prioritize this a little more.

  • Fixed the above vulnerability
  • Redirects & blacklist now properly respect poisoned values
  • Exports and imports now properly respect poisoned values
  • Imported and exported values now register their unimported/unexported counterparts as their real values, which fixes several bugs
  • The environment is now properly imported! Things like rawget and rawset aren’t broken
  • The above also fixed a few potential security holes regarding when the sandbox decides to import values
  • New env option for Sandbox.new()
    • Controls the base environment
  • Add some new sandbox presets
    • Sandbox Sandbox.Empty.new(table options) - Creates a “blank” sandbox with no Instance access (not type based so it can be excepted easily), and has copies of the table, string, math, and os libraries.
      • Also has access to the following functions:
        • ipairs/pairs/next,
        • pcall/xpcall/error/assert
        • print
        • select/unpack
        • type
        • tostring/tonumber
      • Requires in vanilla mode
    • Sandbox Sandbox.Limited.new(table options) - Creates a sandbox with no Instance access and explicitly no access to workspace or game excluding a big list of classes considered safe to use.
      • Notable classes that are not included: RunService, UserInputService, Humanoid, PlayerGui, and bindables and remotes
      • Requires in vanilla mode
    • Sandbox Sandbox.User.new(table options) - Creates a sandbox with no Instance access (type based instead of class based meaning it can’t be excepted normally)
      • Requires in vanilla mode
    • Sandbox Sandbox.Roblox.new(table options) - Creates a sandbox with no access to TeleportService, MarketplaceService, MessagingService, DataStoreService, InsertService and LogService. Blacklists the h6x module.
      • Requires in roblox mode (No asset id access by default)
    • Sandbox Sandbox.Vanilla.new(table options) - Creates a sandbox which mimics a vanilla lua environment. Has a bunch of compatability features. (Most of which are incomplete or not implemented)
      • This is a very insecure sandbox, don’t use it for user code if you want to keep them in the sandbox or stop code from detecting that it is in a sandbox. There are a lot of ways they can escape and probably a lot of bugs they can abuse, not to mention its very different from an actual vanilla environment.
      • Requires in vanilla mode
      • package.loaded is redirected to sandbox.Modules
      • module function can create modules from lua code that can be accessed in the sandbox via require (Like in vanilla lua)
    • Sandbox Sandbox.Plugin.new(table options) - Unfinished. Creates a compatability layer for plugins.
      • Currently just creates an environment and does literally nothing to it
      • Would maybe simulate Instance.RobloxLocked
      • Would maybe simulate Script.Source
      • If running on the client this would create various GUIs (e.g. for dock widgets and toolbars)
        • Would redirect CoreGui to the local PlayerGui
        • Would return the local player’s Mouse instance instead of a PluginMouse
  • Added Unblacklist methods for Class, ClassName, etc (Not sure why I missed these)
  • The sandbox now redirects its base environment to its own
  • Fixed a typo in class type error message
  • Fixed Sandbox:RemoveRedirect() typo
  • Tests are still missing (Oops :flushed:)
    • The reason is because they’re a little more complicated than I anticipated and I didn’t end up having much time to worry about them, and, they’re a relatively low priority
    • Nobody has reported an issue so I am assuming that the feature is just not being used or there aren’t any obvious bugs

Plans

  • Improve and finalize prebuilt sandboxes (Currently they are each mostly unfinished and are pretty lacking)
  • Improve h6x format for activity reporting (make simplified names like in the original example) and fix some issues around it. Also, marking internal calls so they can be optionally hidden from the report would be good to do.
  • Add lua and maybe luac (vanilla lua bytecode) formats
  • Add luau typing for H6x classes
  • Extend this list

Download

H6x Prototype 5-28-21.rbxm (30.5 KB)

1 Like

Thank you for making this! This module has made it possible to develop something I wanted to make for a long time.
Though it lacks a couple of features which I really need: (Please correct me if any of these already exist but I couldn’t find them)

The most important feature that I would really want is the ability to intercept Instance property sets/gets and function calls. For example:

workspace:ClearAllChildren() -- I would need to be able to fully replace this function with my own
workspace.Part.Size = Vector3.new() -- I would need to be able to run code that intercepts this call and has the ability to change what it actually sets to the Size property
print(workspace.Part.Size) -- I would need a function that changes what the .Size returns (ie makes it return a string instead).

Preferably this could be on a per-instance and per-class basis. So I could replace the “ClearAllChildren” function of all models but change the function of workspace to something different.

Another thing that would be useful to me is the ability to add custom properties and methods to individual instances or entire classes.

And a feature that can replace specific Instances (or entire classes, ie all Player objects) with a custom class would be useful too. This would give me full control over what properties and methods the specified instance(s) have and how they are written to, what their values are, or how they are called.

Another feature that would be useful to me is the ability to create custom types. With that, I mean that I could import a class into the sandbox and calling type() on it would not return “table” but rather a customizable string. The same should apply to typeof() too. Maybe this could be done by a __type and __typeof method/string in the imported table’s metatable?

Following onto the last feature request, a feature where I could lock down a table’s metatable entirely would be very useful. Meaning that rawset, rawget, rawequal, getmetatable, and setmetatable (and any other meta-related function I probably forgot) would error when called on said table. Alternatively, meta-methods that change the behavior of all of those functions.

I hope you will consider my suggestions and that what I’m asking for isn’t too much (and that they are even possible).

1 Like

I’m really really happy to hear that this was useful! :grinning_face_with_smiling_eyes:

You can actually do this for functions (in a somewhat limited sense) by redirecting functions in the latest version, so, you’re able to redirect game.ClearAllChildren to a custom function for example, and that custom function can do anything you’d like it to and will be shared across all instances just like they already are (because every time the code accesses Instance.ClearAllChildren, it returns the same exact function, and it gets replaced)

Unfortunately I don’t have a way to add custom properties or methods, but that’s something I’d absolutely love to add thinking about it further, and should not at all be hard since instances are already uniquely wrapped and such.

For your type suggestion, you can take a similar approach and replace the type and typeof functions with your own custom handler. You could have the metatable of the object include a __typeof key which you could return from your custom function, or you could return the default type/typeof result for the value.

Funny little thing about this, each of these are technically already overridden, because the original table is not the one being passed. In fact, all functions retrieved from the global environment are overriden to replace all of the “poison” values with real ones, and then replace each result with poisoned ones.

You can use the redirect feature in the latest version to redirect any of these functions if you want to, and either throw an error or make your own custom behaviour.

I might actually recommend doing something like this if it were possible for you though:

local data = {}
local userdata = newproxy(true) -- Since its a userdata rawget, rawset, getmetatable and setmetatable would fail (But not rawequal)
local meta = getmetatable(userdata)

meta.__index = data -- Example

But, yeah, I’d love to give a way for you to add custom properties and functions to an instance, beyond just overriding the behaviour of existing functions.

Custom classes like you’re mentioning might be something I try to do but I’m not sure. If I do, likely what I will do is have you pass a metatable, and, if there is one the __metatable meta property would be retrieved and replaced with, for example, an empty userdata, and that empty userdata would get mapped with some data including the real and fake metatable, that way H6x code can access the data in the metatable without exposing it to the sandbox, and it would allow getmetatable to be replaced to return the fake one.

1 Like

This might be enough for my use case, but would there be a way to hook into property get/sets? Right now that is the only thing holding me back. Because currently I’m planning on making a custom plugin permission system and for that, I need to restrict properties for specific instances (ie the Source property of scripts). I would need to know when a sandbox attempts to access a property like that so I can prompt the user to allow access and return (in the case of Source) an empty string to not break the plugin. In my specific use case my plan is to split writing and reading source into two different permissions.