feat(lua): 在沙箱中阻止危险的协程创建函数

添加 setupSecureCoroutineLib 函数,在沙箱环境中拦截 coroutine.create/wrap/resume/running,
仅保留 yield/status 安全函数。防止脚本创建不受控制的协程。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-10 14:56:13 +08:00
parent 7a66e350f0
commit aa05d6b965
2 changed files with 201 additions and 0 deletions

View File

@ -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)

View File

@ -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")
}