From 20c4c8f956ba655433c16a34fcf871b62890551a Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:24:47 +0800 Subject: [PATCH] Lua: Hide package from sandboxed scripts --- common/lua/program.go | 17 ++++- common/lua/program_test.go | 124 +++++++++++++++++++++++++++++++++---- 2 files changed, 127 insertions(+), 14 deletions(-) diff --git a/common/lua/program.go b/common/lua/program.go index 559a71d79..506e1d3b4 100644 --- a/common/lua/program.go +++ b/common/lua/program.go @@ -95,6 +95,10 @@ func (p *Program) NewState(ctx context.Context, register func(*glua.LState), val if register != nil { register(L) } + if !p.privileged { + // Host registration needs package.preload; scripts must not access the cache or loaders. + L.SetGlobal(glua.LoadLibName, glua.LNil) + } L.Push(L.NewFunctionFromProto(p.proto)) // Execute the Lua script's top level. if err := L.PCall(0, 0, nil); err != nil { @@ -135,10 +139,19 @@ func newSandboxedState() *glua.LState { L.SetGlobal(name, glua.LNil) } - // require() reads loaders from the registry. + // Capture preload before package is hidden; require() reads loaders from the registry. module := L.GetGlobal(glua.LoadLibName).(*glua.LTable) + preload := module.RawGetString("preload").(*glua.LTable) loaders := L.NewTable() - loaders.RawSetInt(1, module.RawGetString("loaders").(*glua.LTable).RawGetInt(1)) + loaders.RawSetInt(1, L.NewFunction(func(L *glua.LState) int { + name := L.CheckString(1) + loader := preload.RawGetString(name) + if loader == glua.LNil { + loader = glua.LString("no preloaded module: " + name) + } + L.Push(loader) + return 1 + })) module.RawSetString("loaders", loaders) L.SetField(L.Get(glua.RegistryIndex), "_LOADERS", loaders) for _, name := range []string{"loadlib", "seeall", "path", "cpath", "config"} { diff --git a/common/lua/program_test.go b/common/lua/program_test.go index 109376a7e..e8795585d 100644 --- a/common/lua/program_test.go +++ b/common/lua/program_test.go @@ -8,9 +8,11 @@ import ( "errors" "os" "path/filepath" + "runtime" "strings" "testing" "time" + "weak" xbase64 "github.com/xtls/xray-core/common/base64" "github.com/xtls/xray-core/common/platform" @@ -252,20 +254,16 @@ func TestProgramSandbox(t *testing.T) { t.Setenv("LUA_PATH", filepath.ToSlash(filepath.Join(dir, "?.lua"))) program := compileTestProgram(t, ` local function check() + assert(package == nil) assert(require("_G") == _G) for _, name in ipairs({"io", "os", "debug", "channel", "dofile", "loadfile", "load", "loadstring", "getfenv", "setfenv", "collectgarbage", "module", "newproxy", "print", "_printregs"}) do assert(_G[name] == nil, name) end - for _, name in ipairs({"io", "os", "debug", "channel", "escape"}) do - assert(package.loaded[name] == nil and package.preload[name] == nil, name) + for _, name in ipairs({"io", "os", "debug", "channel", "package", "escape"}) do assert(not pcall(require, name), name) end - for name in pairs(package) do - assert(name == "preload" or name == "loaded" or name == "loaders", name) - end - assert(type(package.loaders[1]) == "function" and package.loaders[2] == nil) for _, name in ipairs({"math", "string", "table", "coroutine"}) do assert(require(name) == _G[name], name) end @@ -278,23 +276,41 @@ check() function CheckSandbox() local thread = coroutine.create(check) assert(coroutine.resume(thread)) - package.path = testPath - package.cpath = testPath + local injected = false + package = { + preload = { + escape = function() injected = true; return "escaped" end, + ["xray.late"] = function() injected = true; return "injected" end, + }, + loaded = {escape = "escaped", ["cached.only"] = "injected"}, + loaders = {function() injected = true; return function() return "escaped" end end}, + path = testPath, + cpath = testPath, + } assert(not pcall(require, "escape") and disk_loaded == nil) - package.path = nil - package.cpath = nil + assert(not pcall(require, "cached.only") and not pcall(require, "package")) + assert(require("xray.late") == "host" and require("xray.late") == "host") + assert(require("xray.test").value == "ok" and not injected) + package = nil end`) + var helperLoads, lateLoads int L, err := program.NewState(context.Background(), func(L *glua.LState) { if L.GetGlobal("io") != glua.LNil || L.GetGlobal("loadfile") != glua.LNil { t.Fatal("sandbox was not configured before module registration") } L.SetGlobal("testPath", glua.LString(filepath.ToSlash(filepath.Join(dir, "?.lua")))) L.PreloadModule("xray.test", func(L *glua.LState) int { + helperLoads++ module := L.NewTable() module.RawSetString("value", glua.LString("ok")) L.Push(module) return 1 }) + L.PreloadModule("xray.late", func(L *glua.LState) int { + lateLoads++ + L.Push(glua.LString("host")) + return 1 + }) }, nil) if err != nil { t.Fatal(err) @@ -306,13 +322,96 @@ end`) if err := L.CallByParam(glua.P{Fn: L.GetGlobal("CheckSandbox"), Protect: true}); err != nil { t.Fatal(err) } + if helperLoads != 1 || lateLoads != 1 { + t.Fatalf("module initialization calls = (%d, %d), want (1, 1)", helperLoads, lateLoads) + } +} + +func TestProgramSandboxModuleFailures(t *testing.T) { + program := compileTestProgram(t, ` +assert(package == nil) +local ok, err = pcall(require, "xray.failure") +assert(not ok and string.find(err, "module initialization failed", 1, true)) +package = {loaded = {["xray.failure"] = "injected"}} +ok, err = pcall(require, "xray.failure") +assert(not ok and string.find(err, "loop or previous error loading module", 1, true)) +for i = 1, 2 do + ok, err = pcall(require, "xray.cycle") + assert(not ok and string.find(err, "loop or previous error loading module", 1, true)) +end +package = nil +`) + var failedLoads, cycleLoads int + L, err := program.NewState(context.Background(), func(L *glua.LState) { + L.PreloadModule("xray.failure", func(L *glua.LState) int { + failedLoads++ + L.RaiseError("module initialization failed") + return 0 + }) + L.PreloadModule("xray.cycle", func(L *glua.LState) int { + cycleLoads++ + L.Push(L.GetGlobal("require")) + L.Push(glua.LString("xray.cycle")) + L.Call(1, 1) + return 1 + }) + }, nil) + if err != nil { + t.Fatal(err) + } + defer L.Close() + if failedLoads != 1 || cycleLoads != 1 { + t.Fatalf("module initialization calls = (%d, %d), want (1, 1)", failedLoads, cycleLoads) + } + if L.GetTop() != 0 { + t.Fatalf("stack top = %d, want 0", L.GetTop()) + } +} + +func TestProgramSandboxModulesAreCollected(t *testing.T) { + for _, fail := range []bool{false, true} { + name := "success" + if fail { + name = "failure" + } + t.Run(name, func(t *testing.T) { + ref := func() weak.Pointer[glua.LTable] { + program := compileTestProgram(t, `payload = string.rep("x", 1024 * 1024); assert(pcall(require, "xray.test") == expectedSuccess)`) + L, err := program.NewState(context.Background(), func(L *glua.LState) { + L.SetGlobal("expectedSuccess", glua.LBool(!fail)) + L.PreloadModule("xray.test", func(L *glua.LState) int { + if fail { + L.RaiseError("module initialization failed") + } + L.Push(L.NewTable()) + return 1 + }) + }, nil) + if err != nil { + t.Fatal(err) + } + ref := weak.Make(L.G.Global) + L.Close() + return ref + }() + runtime.GC() + runtime.GC() + if ref.Value() != nil { + t.Fatal("closed sandbox's global table was not collected") + } + }) + } } func TestProgramSandboxClosesFailedInitialization(t *testing.T) { - program := compileTestProgram(t, `require("io")`) + program := compileTestProgram(t, `require("xray.failure")`) var partial *glua.LState L, err := program.NewState(context.Background(), func(L *glua.LState) { partial = L + L.PreloadModule("xray.failure", func(L *glua.LState) int { + L.RaiseError("module initialization failed") + return 0 + }) }, func(*glua.LState) error { t.Error("validation ran after failed initialization") return nil @@ -321,7 +420,7 @@ func TestProgramSandboxClosesFailedInitialization(t *testing.T) { L.Close() t.Fatal("sandbox initialization returned a state") } - if err == nil || partial == nil || !partial.IsClosed() { + if err == nil || !strings.Contains(err.Error(), "module initialization failed") || partial == nil || !partial.IsClosed() { t.Fatalf("error = %v, partial state closed = %t", err, partial != nil && partial.IsClosed()) } } @@ -331,6 +430,7 @@ func TestProgramPrivilegedLibraries(t *testing.T) { const code = `-- @xray:privileged-psk=secret assert(require("io") == io and require("os") == os) assert(require("debug") == debug and require("channel") == channel) +assert(type(coroutine.create) == "function" and require("coroutine") == coroutine) assert(type(dofile) == "function" and type(loadfile) == "function") assert(type(getfenv) == "function" and type(setfenv) == "function") assert(type(collectgarbage) == "function" and type(print) == "function")