Getting Started
Security

Security

When running lune scripts, it is important to note that any scripts you execute have full access to your device, including access to your files, programs, and more. Therefore, it is important to stay cautious when executing a script from a source you don't trust.

If you are unsure what a script does, it is recommended to run the script in a sandboxed (opens in a new tab) environment to evaluate what it does.

Lune does not include a built-in permissions sandbox, but the following luau script, which utilizes the @lune/luau library can be used.

Step 1

Copy the source below and place it in a file named sandbox.luau:

Click to expand
local datetime = require("@lune/datetime")
local fs = require("@lune/fs")
local luau = require("@lune/luau")
local net = require("@lune/net")
local process = require("@lune/process")
local regex = require("@lune/regex")
local roblox = require("@lune/roblox")
local serde = require("@lune/serde")
local stdio = require("@lune/stdio")
local task = require("@lune/task")
 
local processArgs = table.clone(process.args)
local filePath: string = table.remove(processArgs, 1)
	or error("usage: lune run sandbox [SCRIPT_PATH] -- [ARGS]")
 
local DEFAULT_PRINT = print
local SANDBOXED_ENV = {
	debugName = filePath,
	environment = {
		require = nil,
		getfenv = nil,
		setfenv = nil,
		print = nil,
		warn = nil,
	},
}
 
local PROMPT_MSG_TMPL = `allow {SANDBOXED_ENV.debugName} to access %s?`
local DENIED_ERR_TMPL = `{SANDBOXED_ENV.debugName} tried to access disallowed library %s!`
 
local function discoverAndReadScript(filePath: string): string
	local scriptContents: string
 
	if fs.isFile(filePath) then
		scriptContents = fs.readFile(filePath)
		return scriptContents
	end
 
	if fs.isDir(filePath) then
		if fs.isFile(filePath .. "/init.luau") then
			scriptContents = fs.readFile(filePath .. "/init.luau")
		end
 
		if fs.isFile(filePath .. "/init.lua") then
			scriptContents = fs.readFile(filePath .. "/init.lua")
		end
	end
 
	if scriptContents == nil then
		for _, ext in { ".luau", ".lua" } do
			local filePathExt = filePath .. ext
			if fs.isFile(filePathExt) then
				scriptContents = fs.readFile(filePathExt)
			end
		end
 
		if scriptContents == nil then
			error(`No such file or directory \`{filePath}\``)
		end
	end
 
	return scriptContents
end
 
local function sandboxGetfenv(): {}
	if table.isfrozen(SANDBOXED_ENV) then
		return SANDBOXED_ENV.environment
	end
 
	return {}
end
 
local function sandboxSetfenv(env: {}): never
	error("cannot call setfenv from sandbox")
end
 
local function sandboxPrint(...: any)
	DEFAULT_PRINT(`---- Output from {SANDBOXED_ENV.debugName} ----`)
	DEFAULT_PRINT(tostring(...))
	DEFAULT_PRINT(`---------------------------------------`)
end
 
local function constructProtectedMt<T>(library: T)
	return {
		__index = library,
		__metatable = "Locked",
		__tostring = function()
			return stdio.format(library)
		end,
	}
end
 
local SANDBOXED_LUNE_STD_LIB = {
	["@lune/fs"] = setmetatable({}, constructProtectedMt(fs)),
	["@lune/luau"] = setmetatable({}, constructProtectedMt(luau)),
	["@lune/process"] = setmetatable({}, constructProtectedMt(process)),
	["@lune/stdio"] = setmetatable({
		write = sandboxPrint,
		ewrite = sandboxPrint,
	}, constructProtectedMt(stdio)),
	["@lune/net"] = setmetatable({}, constructProtectedMt(net)),
	["@lune/roblox"] = setmetatable({
		getAuthCookie = function(...)
			local allowAuthCookie: boolean = stdio.prompt(
				"confirm",
				`allow {SANDBOXED_ENV.debugName} to access your .ROBLOSECURITY token?`
			)
 
			if allowAuthCookie then
				local getAuthCookie = require("@lune/roblox").getAuthCookie
				return getAuthCookie(...)
			end
 
			error(
				`{SANDBOXED_ENV.debugName} attempted to access .ROBLOSECURITY token even when denied`
			)
		end,
	}, constructProtectedMt(roblox)),
	["@lune/serde"] = serde,
	["@lune/task"] = task,
	["@lune/regex"] = regex,
	["@lune/datetime"] = datetime,
}
 
local function sandboxedRequire(path: string)
	local module = SANDBOXED_LUNE_STD_LIB[path]
 
	if module then
		local allowed: boolean = stdio.prompt("confirm", string.format(PROMPT_MSG_TMPL, path))
 
		if allowed then
			return module
		end
 
		error(string.format(DENIED_ERR_TMPL, path))
	else
		local contents = discoverAndReadScript(path)
 
		local evalChunk = luau.load(contents, SANDBOXED_ENV)
		return evalChunk()
	end
end
 
SANDBOXED_ENV.environment.require = sandboxedRequire
SANDBOXED_ENV.environment.getfenv = sandboxGetfenv
SANDBOXED_ENV.environment.setfenv = sandboxSetfenv
SANDBOXED_ENV.environment.print = sandboxPrint
SANDBOXED_ENV.environment.warn = sandboxPrint
luau.load(discoverAndReadScript(filePath), table.freeze(SANDBOXED_ENV))()

Step 2

Now, place the untrusted script you want to run safely next to the sandbox.luau script.

Bash
lune run sandbox.luau script.luau -- [ARGUMENTS_HERE]

Replace script.luau and [ARGUMENTS_HERE] with the path to the script and the arguments to run it with.

Step 3

As the script runs, any requires to potentially dangerous modules will require your approval before continuing and any invocations to methods within approved modules will be logged.

Furthermore, the output of the sandbox script and the script being run will be separated.