mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-15 03:53:59 +00:00
lua: redact memory addresses in sandbox errors
This commit is contained in:
+39
-2
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -128,10 +129,10 @@ func newSandboxedState() *glua.LState {
|
||||
result = value.String()
|
||||
case glua.LTUserData:
|
||||
switch native := value.(*glua.LUserData).Value.(type) {
|
||||
case error: // Errors can also implement fmt.Stringer; redact them first.
|
||||
result = redactMemoryAddresses(native.Error())
|
||||
case fmt.Stringer:
|
||||
result = native.String()
|
||||
case error:
|
||||
result = native.Error()
|
||||
}
|
||||
}
|
||||
return glua.LString(result)
|
||||
@@ -160,6 +161,36 @@ func newSandboxedState() *glua.LState {
|
||||
return 1
|
||||
}))
|
||||
|
||||
// Redact addresses before caught errors become readable by the script.
|
||||
redactError := func(value glua.LValue) glua.LValue {
|
||||
if message, ok := value.(glua.LString); ok {
|
||||
return glua.LString(redactMemoryAddresses(string(message)))
|
||||
}
|
||||
return value
|
||||
}
|
||||
for _, name := range []string{"pcall", "xpcall"} {
|
||||
call := L.GetGlobal(name)
|
||||
L.SetGlobal(name, L.NewFunction(func(L *glua.LState) int {
|
||||
nargs := L.GetTop()
|
||||
if name == "xpcall" {
|
||||
L.CheckFunction(1)
|
||||
handler := L.CheckFunction(2)
|
||||
L.Replace(2, L.NewFunction(func(L *glua.LState) int {
|
||||
L.Replace(1, redactError(L.Get(1)))
|
||||
L.Insert(handler, 1)
|
||||
L.Call(1, 1)
|
||||
return 1
|
||||
}))
|
||||
}
|
||||
L.Insert(call, 1)
|
||||
L.Call(nargs, glua.MultRet)
|
||||
if L.Get(1) == glua.LFalse {
|
||||
L.Replace(2, redactError(L.Get(2)))
|
||||
}
|
||||
return L.GetTop()
|
||||
}))
|
||||
}
|
||||
|
||||
// Prevent seed output from compromising cryptographic security.
|
||||
L.GetGlobal(glua.MathLibName).(*glua.LTable).RawSetString("randomseed", glua.LNil)
|
||||
|
||||
@@ -185,6 +216,12 @@ func newSandboxedState() *glua.LState {
|
||||
return L
|
||||
}
|
||||
|
||||
var memoryAddressPattern = regexp.MustCompile(`0[xX][0-9a-fA-F]+`)
|
||||
|
||||
func redactMemoryAddresses(message string) string {
|
||||
return memoryAddressPattern.ReplaceAllString(message, "<address>")
|
||||
}
|
||||
|
||||
// NewStateFactory returns a factory that gives each state an initialization timeout.
|
||||
func (p *Program) NewStateFactory(initTimeout time.Duration, register func(*glua.LState), validate func(*glua.LState) error) LStateFactory {
|
||||
return func(ctx context.Context) (*glua.LState, error) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"time"
|
||||
"weak"
|
||||
|
||||
xerrors "github.com/xtls/xray-core/common/errors"
|
||||
glua "github.com/yuin/gopher-lua"
|
||||
luar "layeh.com/gopher-luar"
|
||||
)
|
||||
@@ -631,6 +633,7 @@ for _, convert in ipairs({tostring, require("_G").tostring}) do
|
||||
assert(convert(true) == "true" and convert(false) == "false")
|
||||
assert(convert(0) == "0" and convert(-42) == "-42" and convert(1.25) == "1.25")
|
||||
assert(convert("") == "" and convert("hello") == "hello")
|
||||
assert(convert("0xdeadbeef") == "0xdeadbeef")
|
||||
assert(convert(42, {}) == "42")
|
||||
assert(not pcall(convert))
|
||||
assert(convert(testIPv4) == "192.0.2.1")
|
||||
@@ -695,6 +698,7 @@ assert(string.format == require("string").format and string.format == ("").forma
|
||||
for _, format in ipairs({string.format, require("string").format, ("").format}) do
|
||||
assert(format("%s %04d %.2f %t", "hello", 42, 1.25, true) == "hello 0042 1.25 true")
|
||||
assert(format("%q", "hello") == '"hello"')
|
||||
assert(format("%s", "0xdeadbeef") == "0xdeadbeef")
|
||||
assert(format("%[2]s:%[1]d", 42, "hello") == "hello:42")
|
||||
assert(format("%%p") == "%p")
|
||||
assert(format("%%p", {}) == format("%%p", "<opaque>"))
|
||||
@@ -754,6 +758,189 @@ assert(("%s"):format({}) == "<opaque>")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgramSandboxProtectedCalls(t *testing.T) {
|
||||
program := compileTestProgram(t, `
|
||||
assert(pcall == require("_G").pcall and xpcall == require("_G").xpcall)
|
||||
local object = {}
|
||||
local function succeed(a, b, c)
|
||||
assert(a == 42 and b == nil and c == object)
|
||||
return nil, object, "0xdeadbeef"
|
||||
end
|
||||
local ok, a, b, c = pcall(succeed, 42, nil, object)
|
||||
assert(ok and a == nil and b == object and c == "0xdeadbeef")
|
||||
assert(select("#", pcall(succeed, 42, nil, object)) == 4)
|
||||
local function noArguments(...)
|
||||
assert(select("#", ...) == 0)
|
||||
return nil, object, nil
|
||||
end
|
||||
local function unexpectedHandler() error("handler must not run") end
|
||||
ok, a, b, c = xpcall(noArguments, unexpectedHandler, "ignored")
|
||||
assert(ok and a == nil and b == object and c == nil)
|
||||
assert(select("#", xpcall(noArguments, unexpectedHandler)) == 4)
|
||||
ok, a = pcall(testCallable, 42)
|
||||
assert(ok and a == 42)
|
||||
ok, a = pcall(nil)
|
||||
assert(not ok and a == "attempt to call a nil value")
|
||||
ok, a = pcall(pcall)
|
||||
assert(not ok and string.find(a, "bad argument #1", 1, true))
|
||||
ok, a = pcall(xpcall, nil, nil)
|
||||
assert(not ok and string.find(a, "bad argument #1", 1, true))
|
||||
ok, a = pcall(xpcall, noArguments, nil)
|
||||
assert(not ok and string.find(a, "bad argument #2", 1, true))
|
||||
|
||||
local businessError = "DNS lookup timed out (192.0.2.1)"
|
||||
ok, a = pcall(error, businessError, 0)
|
||||
assert(not ok and a == businessError)
|
||||
ok, a = xpcall(function() error(businessError, 0) end, function(message)
|
||||
assert(message == businessError)
|
||||
return "handled: " .. message, "ignored"
|
||||
end)
|
||||
assert(not ok and a == "handled: " .. businessError)
|
||||
local message = "lookup at 0x12Ab / 0XFE90; 2001:db8::1; 0x"
|
||||
local redacted = "lookup at <address> / <address>; 2001:db8::1; 0x"
|
||||
ok, a = pcall(error, message, 0)
|
||||
assert(not ok and a == redacted)
|
||||
ok, a = pcall(error, "business id=0xdeadbeef", 0)
|
||||
assert(not ok and a == "business id=<address>")
|
||||
for _, value in ipairs({object, testObject, true, 42}) do
|
||||
ok, a = pcall(error, value, 0)
|
||||
assert(not ok and a == value)
|
||||
ok, a = xpcall(function() error(value, 0) end, function(caught)
|
||||
assert(caught == value)
|
||||
return caught
|
||||
end)
|
||||
assert(not ok and a == value)
|
||||
end
|
||||
ok, a = pcall(error, nil, 0)
|
||||
assert(not ok and a == nil)
|
||||
|
||||
local conversion = "cannot use function: <address> (type *lua.LFunction) as type string"
|
||||
ok, a = pcall(testTakeString, function() end)
|
||||
assert(not ok and string.find(a, conversion, 1, true))
|
||||
ok, a = xpcall(function() testTakeString(function() end) end, function(caught)
|
||||
testSendError(caught)
|
||||
return caught
|
||||
end)
|
||||
assert(not ok and string.find(a, conversion, 1, true))
|
||||
ok, a = pcall(function() testValues[1] = testObject end)
|
||||
assert(not ok and string.find(a, "<address>", 1, true) and not string.find(a, "0x", 1, true))
|
||||
assert(string.find(a, "as type int", 1, true))
|
||||
testValues[1] = 7
|
||||
local extended = testValues + 9
|
||||
assert(testValues[1] == 7 and #extended == 2 and extended[2] == 9)
|
||||
ok, a = pcall(testPanic)
|
||||
assert(not ok and a == "{<address> <address>}")
|
||||
ok, a = xpcall(testPanic, function(caught)
|
||||
testSendError(caught)
|
||||
return caught
|
||||
end)
|
||||
assert(not ok and a == "{<address> <address>}")
|
||||
ok, a = xpcall(function() error(message, 0) end, function(caught)
|
||||
assert(caught == redacted)
|
||||
error("handler failed at 0xABCD", 0)
|
||||
end)
|
||||
assert(not ok and a == "handler failed at <address>")
|
||||
ok, a = xpcall(function() error(businessError, 0) end, testPanic)
|
||||
assert(not ok and a == "{<address> <address>}")
|
||||
`)
|
||||
var sent []string
|
||||
L, err := program.NewState(context.Background(), func(L *glua.LState) {
|
||||
callable := L.NewTable()
|
||||
metatable := L.NewTable()
|
||||
metatable.RawSetString("__call", L.NewFunction(func(L *glua.LState) int {
|
||||
L.Push(L.Get(2))
|
||||
return 1
|
||||
}))
|
||||
L.SetMetatable(callable, metatable)
|
||||
L.SetGlobal("testCallable", callable)
|
||||
L.SetGlobal("testTakeString", luar.New(L, func(string) {}))
|
||||
target := 42
|
||||
PushUserData(L, struct{ Pointer *int }{&target})
|
||||
L.SetGlobal("testObject", L.Get(-1))
|
||||
L.Pop(1)
|
||||
NewSlicePusher[int](L)(L, []int{1})
|
||||
L.SetGlobal("testValues", L.Get(-1))
|
||||
L.Pop(1)
|
||||
L.SetGlobal("testPanic", L.NewFunction(func(L *glua.LState) int {
|
||||
panic(struct{ First, Second *int }{&target, &target})
|
||||
}))
|
||||
L.SetGlobal("testSendError", L.NewFunction(func(L *glua.LState) int {
|
||||
message := L.CheckString(1)
|
||||
if strings.Contains(message, "0x") || strings.Contains(message, "0X") {
|
||||
t.Errorf("outbound error contains an address: %q", message)
|
||||
}
|
||||
sent = append(sent, message)
|
||||
return 0
|
||||
}))
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer L.Close()
|
||||
if len(sent) != 2 || !strings.Contains(sent[0], "cannot use function: <address>") || sent[1] != "{<address> <address>}" {
|
||||
t.Fatalf("outbound errors = %q", sent)
|
||||
}
|
||||
if L.GetTop() != 0 {
|
||||
t.Fatalf("stack top = %d, want 0", L.GetTop())
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgramSandboxGoErrorStrings(t *testing.T) {
|
||||
program := compileTestProgram(t, `
|
||||
for name, expected in pairs(testErrorMessages) do
|
||||
local value = _G[name]
|
||||
assert(tostring(value) == expected, name)
|
||||
assert(string.format("%s", value) == expected, name)
|
||||
assert(string.format("%p", value) == string.format("%p", expected), name)
|
||||
assert(string.format("%#v", value) == string.format("%#v", expected), name)
|
||||
local ok, caught = pcall(error, value, 0)
|
||||
assert(not ok and caught == value and tostring(caught) == expected, name)
|
||||
ok, caught = xpcall(function() error(value, 0) end, function(caught)
|
||||
assert(caught == value and tostring(caught) == expected, name)
|
||||
return caught
|
||||
end)
|
||||
assert(not ok and caught == value, name)
|
||||
end
|
||||
`)
|
||||
nativeErrors := make(map[string]error)
|
||||
originalMessages := make(map[string]string)
|
||||
L, err := program.NewState(context.Background(), func(L *glua.LState) {
|
||||
luaError := &glua.ApiError{Object: L.NewFunction(func(*glua.LState) int { return 0 })}
|
||||
stringerError := xerrors.New("DNS query failed").Base(luaError)
|
||||
messages := L.NewTable()
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
err error
|
||||
expected string
|
||||
}{
|
||||
{"nativeError", errors.New("lookup failed at 0x12Ab and 0XFe90"), "lookup failed at <address> and <address>"},
|
||||
{"luaError", luaError, "function: <address>"},
|
||||
{"wrappedError", fmt.Errorf("DNS query failed: %w", luaError), "DNS query failed: function: <address>"},
|
||||
{"stringerError", stringerError, strings.ReplaceAll(stringerError.Error(), luaError.Object.String(), "function: <address>")},
|
||||
} {
|
||||
nativeErrors[tc.name] = tc.err
|
||||
originalMessages[tc.name] = tc.err.Error()
|
||||
PushError(L, tc.err)
|
||||
L.SetGlobal(tc.name, L.Get(-1))
|
||||
L.Pop(1)
|
||||
messages.RawSetString(tc.name, glua.LString(tc.expected))
|
||||
}
|
||||
L.SetGlobal("testErrorMessages", messages)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer L.Close()
|
||||
for name, native := range nativeErrors {
|
||||
if got := ReadError(L.GetGlobal(name), "invalid"); got != native {
|
||||
t.Fatalf("%s error identity changed", name)
|
||||
}
|
||||
if got := native.Error(); got != originalMessages[name] {
|
||||
t.Fatalf("%s host error = %q, want %q", name, got, originalMessages[name])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgramSandboxModuleFailures(t *testing.T) {
|
||||
program := compileTestProgram(t, `
|
||||
assert(package == nil)
|
||||
@@ -864,6 +1051,13 @@ assert(type(getmetatable) == "function" and type(setmetatable) == "function")
|
||||
assert(type(rawget) == "function" and type(rawset) == "function")
|
||||
assert(tostring(42) == "42" and type(tostring({})) == "string")
|
||||
assert(tostring(setmetatable({}, {__tostring = function() return "custom" end})) == "custom")
|
||||
local ok, message = pcall(error, "lookup failed at 0x12Ab", 0)
|
||||
assert(not ok and message == "lookup failed at 0x12Ab")
|
||||
ok, message = xpcall(function() error("lookup failed at 0x12Ab", 0) end, function(caught)
|
||||
assert(caught == "lookup failed at 0x12Ab")
|
||||
return caught
|
||||
end)
|
||||
assert(not ok and message == "lookup failed at 0x12Ab")
|
||||
assert(type(string.dump) == "function" and type(math.randomseed) == "function")
|
||||
assert(type(package.loaders[2]) == "function" and type(package.path) == "string")
|
||||
assert(type(debug.traceback()) == "string" and type(os.time()) == "number")
|
||||
|
||||
Reference in New Issue
Block a user