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:
parent
7a66e350f0
commit
aa05d6b965
@ -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)
|
||||
|
||||
155
internal/lua/security_test.go
Normal file
155
internal/lua/security_test.go
Normal 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")
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user