From aa05d6b965510439e4f534fa84b5cdca1f4349a8 Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 10 Apr 2026 14:56:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(lua):=20=E5=9C=A8=E6=B2=99=E7=AE=B1?= =?UTF-8?q?=E4=B8=AD=E9=98=BB=E6=AD=A2=E5=8D=B1=E9=99=A9=E7=9A=84=E5=8D=8F?= =?UTF-8?q?=E7=A8=8B=E5=88=9B=E5=BB=BA=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 setupSecureCoroutineLib 函数,在沙箱环境中拦截 coroutine.create/wrap/resume/running, 仅保留 yield/status 安全函数。防止脚本创建不受控制的协程。 Co-Authored-By: Claude Opus 4.6 --- internal/lua/coroutine.go | 46 ++++++++++ internal/lua/security_test.go | 155 ++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 internal/lua/security_test.go diff --git a/internal/lua/coroutine.go b/internal/lua/coroutine.go index 7e2e639..9cbda30 100644 --- a/internal/lua/coroutine.go +++ b/internal/lua/coroutine.go @@ -101,9 +101,55 @@ func (c *LuaCoroutine) SetupSandbox() error { // 将 _ENV 设置到协程 c.Co.SetGlobal("_ENV", env) + // Layer 1 & 2: 设置安全的协程库(移除危险函数) + c.setupSecureCoroutineLib() + return nil } +// setupSecureCoroutineLib 创建安全的协程库替换 +// 移除 coroutine.create/wrap/resume,仅保留 yield/status +func (c *LuaCoroutine) setupSecureCoroutineLib() { + // 获取原始 coroutine 表 + originalCoroutine := c.Engine.L.GetGlobal("coroutine") + if originalCoroutine == glua.LNil { + return // coroutine 库未加载 + } + + origTable, ok := originalCoroutine.(*glua.LTable) + if !ok { + return + } + + // 创建安全的 coroutine 表 + safeCoroutine := c.Co.NewTable() + + // 仅保留安全的函数:yield 和 status + if yield := origTable.RawGetString("yield"); yield != glua.LNil { + safeCoroutine.RawSetString("yield", yield) + } + if status := origTable.RawGetString("status"); status != glua.LNil { + safeCoroutine.RawSetString("status", status) + } + + // 拦截函数 - 返回友好错误 + blockFn := c.Co.NewFunction(func(L *glua.LState) int { + L.RaiseError("coroutine creation is blocked in sandbox (use engine-provided coroutine instead)") + return 0 + }) + safeCoroutine.RawSetString("create", blockFn) + safeCoroutine.RawSetString("wrap", blockFn) + safeCoroutine.RawSetString("resume", blockFn) + safeCoroutine.RawSetString("running", blockFn) // 防止信息泄露 + + // 替换协程的 coroutine 全局变量 + c.Co.SetGlobal("coroutine", safeCoroutine) + + // 注意:不修改引擎级全局表 origTable,避免并发竞态条件 + // _G.coroutine 的访问通过沙箱的 __index 元表机制被隔离 + // 因为协程继承的是引擎全局环境,而我们在协程级别设置了独立的 coroutine 表 +} + // Execute 在协程中执行 Lua 脚本(支持 Yield/Resume) func (c *LuaCoroutine) Execute(script string) error { proto, err := c.Engine.codeCache.GetOrCompileInline(script) diff --git a/internal/lua/security_test.go b/internal/lua/security_test.go new file mode 100644 index 0000000..49afcc0 --- /dev/null +++ b/internal/lua/security_test.go @@ -0,0 +1,155 @@ +// Package lua 提供 Lua 脚本嵌入能力 +package lua + +import ( + "testing" + + glua "github.com/yuin/gopher-lua" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSandboxBlocksCoroutineCreate 验证协程创建被阻止 +func TestSandboxBlocksCoroutineCreate(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + // 测试直接调用被阻止 + coro1, err := engine.NewCoroutine(nil) + require.NoError(t, err) + err = coro1.SetupSandbox() + require.NoError(t, err) + err = coro1.Execute(`coroutine.create(function() end)`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "blocked") + coro1.Close() + + // 测试通过 _G 访问被阻止(需要新协程,因为前一个已 dead) + coro2, err := engine.NewCoroutine(nil) + require.NoError(t, err) + err = coro2.SetupSandbox() + require.NoError(t, err) + err = coro2.Execute(`_G.coroutine.create(function() end)`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "blocked") + coro2.Close() +} + +// TestGlobalCoroutineBypassAttempt 验证 _G.coroutine 绕过尝试失败 +func TestGlobalCoroutineBypassAttempt(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + // 测试 _G.coroutine.create 绕过 + coro1, err := engine.NewCoroutine(nil) + require.NoError(t, err) + err = coro1.SetupSandbox() + require.NoError(t, err) + err = coro1.Execute(`_G.coroutine.create(function() end)`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "blocked") + coro1.Close() + + // 测试字符串索引绕过 + coro2, err := engine.NewCoroutine(nil) + require.NoError(t, err) + err = coro2.SetupSandbox() + require.NoError(t, err) + err = coro2.Execute(`local c = _G["coroutine"]; c.create(function() end)`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "blocked") + coro2.Close() +} + +// TestSandboxBlocksCoroutineWrap 验证 coroutine.wrap 被阻止 +func TestSandboxBlocksCoroutineWrap(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + err = coro.SetupSandbox() + require.NoError(t, err) + + err = coro.Execute(`coroutine.wrap(function() end)`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "blocked") +} + +// TestSandboxPreservesYield 验证 yield 正常工作 +func TestSandboxPreservesYield(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + err = coro.SetupSandbox() + require.NoError(t, err) + + // yield 应该正常工作(由引擎控制) + err = coro.Execute(`coroutine.yield("sleep", 0.001)`) + assert.NoError(t, err) +} + +// TestSandboxPreservesStatus 验证 status 可用 +func TestSandboxPreservesStatus(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + err = coro.SetupSandbox() + require.NoError(t, err) + + // status 应该可用 + err = coro.Execute(`local s = coroutine.status; return s`) + assert.NoError(t, err) +} + +// TestDebugLibraryNotLoaded 验证 debug 库未加载 +func TestDebugLibraryNotLoaded(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + // debug 库应该不存在 + debug := engine.L.GetGlobal("debug") + assert.Equal(t, glua.LNil, debug, "debug library should not be loaded") + + // 尝试访问 debug.getregistry 应该失败 + err = coro.Execute(`return debug.getregistry()`) + assert.Error(t, err) +} + +// TestCoroutineRunningBlocked 验证 coroutine.running 被阻止(防止信息泄露) +func TestCoroutineRunningBlocked(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + err = coro.SetupSandbox() + require.NoError(t, err) + + err = coro.Execute(`coroutine.running()`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "blocked") +} \ No newline at end of file