mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
用户要求:导入适配器时必须检查路径冲突,避免静默错误。 之前 UploadAdapter(name, code) 直接拼 adapter_dir/<name>.lua: - name 含 "../" 或绝对路径会写到目录外 - name 与内置同名会静默覆盖,加载失败只报 "adapter X missing", 现场与病因分离 实现(internal/lua/adapter_validate.go): - ValidateAdapterName:字符集 ^[a-zA-Z0-9_-]+$,拒绝 /、\、..、绝对路径、空白 - ValidateAdapterPath:文件必须在 adapter_dir 内 - VM.InstallPluginAdapter:插件装内置名 → ErrAdapterNameReserved 拒 - Core.UploadAdapter:在 WriteFile 之前校验(防路径逃逸) - Core.RemoveAdapter:也校验名字(防 os.Remove 越界) 设计取舍:保留名禁止只对插件生效,不对运维生效。 - operator 放 openai.lua 覆盖内置 = 文档化的热覆盖机制,放行 - 第三方插件静默换掉内核自带的协议翻译 = 篡改发行物,拒绝 - 先误做成了全拒,Start() 加载内置适配器直接崩,测试抓住后改对 (TestLoadBundledAdapters 等锁住回归) 测试 12 个:路径穿越/绝对路径/保留段/空白/内置覆盖(允许 vs 插件拒绝)/ 目录外加载/错误点名/热重载不回归。 验证:go test -tags luajit ./... 全绿。
198 lines
6.9 KiB
Go
198 lines
6.9 KiB
Go
package lua
|
||
|
||
import (
|
||
"errors"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// 本文件锁定 R6:导入适配器时检查路径冲突,避免静默错误。
|
||
//
|
||
// 覆盖三类注入:
|
||
// 1. 名字字符集(路径穿越 / 绝对路径 / 空白)→ 拒绝
|
||
// 2. 保留名冲突(内置适配器)→ 拒绝
|
||
// 3. 合法名 → 通过(且确实可加载)
|
||
|
||
func TestValidateAdapterNameRejectsPathTraversal(t *testing.T) {
|
||
for _, name := range []string{
|
||
"../openai", // 目录逃逸
|
||
"..", // 保留段
|
||
".", // 保留段
|
||
"/etc/passwd", // 绝对路径
|
||
"a/b/c", // 斜杠
|
||
"a\\b", // 反斜杠
|
||
"has space", // 空白
|
||
"a b c", // 空格
|
||
"", // 空
|
||
} {
|
||
if err := ValidateAdapterName(name, nil); err == nil {
|
||
t.Errorf("名字 %q 应被拒绝(路径不安全),但通过了", name)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestValidateAdapterNameRejectsReserved(t *testing.T) {
|
||
reserved := []string{"openai", "anthropic", "gemini", "Ollama"}
|
||
for _, name := range []string{"openai", "anthropic", "gemini", "ollama"} {
|
||
if err := ValidateAdapterName(name, reserved); err == nil {
|
||
t.Errorf("名字 %q 与内置适配器冲突,应被拒绝", name)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestValidateAdapterNameAcceptsLegal(t *testing.T) {
|
||
for _, name := range []string{
|
||
"my_adapter",
|
||
"my-adapter",
|
||
"custom123",
|
||
"a",
|
||
"opencode", // 不是保留名(本次已从内置移除)
|
||
} {
|
||
if err := ValidateAdapterName(name, []string{"openai", "anthropic"}); err != nil {
|
||
t.Errorf("合法名字 %q 被误拒: %v", name, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestValidateAdapterPathRejectsOutsideDir 验证从磁盘加载路径时,
|
||
// 目录外的文件被拒绝。
|
||
func TestValidateAdapterPathRejectsOutsideDir(t *testing.T) {
|
||
dir := t.TempDir()
|
||
outside := filepath.Join(dir, "..", "evil.lua")
|
||
if _, err := ValidateAdapterPath(outside, dir, nil); err == nil {
|
||
t.Errorf("目录外的路径 %q 应被拒绝", outside)
|
||
}
|
||
}
|
||
|
||
// TestLoadAdapterSourceAllowsBuiltinOverride 验证运维覆盖内置适配器是允许的
|
||
// (文档化的 adapter_dir 热覆盖机制)——仅插件被禁止影子内置。
|
||
func TestLoadAdapterSourceAllowsBuiltinOverride(t *testing.T) {
|
||
vm := NewVM(filepath.Join(t.TempDir(), "adapters"))
|
||
if err := vm.Start(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer vm.Stop()
|
||
|
||
code := `local a={} a.name="openai" a.version="override" a.endpoint="/x" return a`
|
||
if err := vm.LoadAdapterSource("openai", code); err != nil {
|
||
t.Errorf("运维覆盖内置 openai 应被允许(热覆盖机制),却被拒: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestInstallPluginAdapterRejectsBuiltin 验证**插件**装内置名被拒(防篡改)。
|
||
func TestInstallPluginAdapterRejectsBuiltin(t *testing.T) {
|
||
vm := NewVM(filepath.Join(t.TempDir(), "adapters"))
|
||
if err := vm.Start(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer vm.Stop()
|
||
|
||
code := `local a={} a.name="openai" a.version="evil" a.endpoint="/x" return a`
|
||
if err := vm.InstallPluginAdapter("openai", code); err == nil {
|
||
t.Error("插件装与内置同名的适配器应被拒(防篡改发行物)")
|
||
}
|
||
// 但插件装非内置名应成功
|
||
ok := `local a={} a.name="myplugin_adapter" a.version="1" a.endpoint="/x" return a`
|
||
if err := vm.InstallPluginAdapter("myplugin_adapter", ok); err != nil {
|
||
t.Errorf("插件装非保留名应成功: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestLoadAdapterSourceRejectsReserved(t *testing.T) {
|
||
t.Skip("已改:LoadAdapterSource 允许运维覆盖内置;保留名禁止已移至 InstallPluginAdapter")
|
||
}
|
||
|
||
// TestLoadAdapterSourceRejectsTraversal 验证加载入口拒绝路径穿越名。
|
||
func TestLoadAdapterSourceRejectsTraversal(t *testing.T) {
|
||
vm := NewVM(filepath.Join(t.TempDir(), "adapters"))
|
||
if err := vm.Start(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer vm.Stop()
|
||
|
||
code := `local a={} a.name="x" a.version="1" a.endpoint="/x" return a`
|
||
if err := vm.LoadAdapterSource("../evil", code); err == nil {
|
||
t.Error("路径穿越名 ../evil 应被拒绝")
|
||
}
|
||
}
|
||
|
||
// TestUploadAdapterPathTraversalDoesNotEscape 是端到端安全测试:
|
||
// 模拟管理员通过 API 上传一个恶意名,验证它**不会**写到 adapter_dir 之外。
|
||
func TestUploadAdapterPathTraversalDoesNotEscape(t *testing.T) {
|
||
// 用一个真实 Core 来测 UploadAdapter,但 core 需要完整配置——这里直接
|
||
// 测 UploadAdapter 的校验层(名字在写文件之前就被拒)。
|
||
// 由于 UploadAdapter 在 core 包,我们在 lua 层能测的等价面是
|
||
// ValidateAdapterName + LoadAdapterSource,上面已覆盖。
|
||
// 这里补一个"确无副作用"断言:遍历名不得创建任何文件。
|
||
dir := filepath.Join(t.TempDir(), "adapters")
|
||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := ValidateAdapterName("../openai", nil); err == nil {
|
||
t.Fatal("前提不成立")
|
||
}
|
||
// 证明没有 ../openai.lua 被创建
|
||
if _, err := os.Stat(filepath.Join(dir, "..", "openai.lua")); !os.IsNotExist(err) {
|
||
t.Error("校验前就写出了逃逸文件 —— 写入顺序错误")
|
||
}
|
||
}
|
||
|
||
// TestLoadAdapterValidatesTheDiskPath 验证从磁盘加载(Start 的路径)对路径
|
||
// 做安全校验:目录外的文件被拒。(目录内同名内置的文件是合法覆盖,
|
||
// 不在此拒绝——见 TestLoadAdapterSourceAllowsBuiltinOverride。)
|
||
func TestLoadAdapterValidatesTheDiskPath(t *testing.T) {
|
||
dir := filepath.Join(t.TempDir(), "adapters")
|
||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// 目录外的文件:应被拒
|
||
outside := filepath.Join(dir, "..", "evil.lua")
|
||
if err := os.WriteFile(outside, []byte(`local a={} a.name="evil" a.version="1" a.endpoint="/x" return a`), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
vm := NewVM(dir)
|
||
if err := vm.LoadAdapter(outside); err == nil {
|
||
t.Error("目录外的磁盘文件应被拒绝")
|
||
}
|
||
}
|
||
|
||
// TestValidateAdapterPathInDirAccepted 验证目录内的合法文件通过。
|
||
func TestValidateAdapterPathInDirAccepted(t *testing.T) {
|
||
dir := filepath.Join(t.TempDir(), "adapters")
|
||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
ok := filepath.Join(dir, "my_new.lua")
|
||
if err := os.WriteFile(ok, []byte(`local a={} a.name="my_new" a.version="1" a.endpoint="/x" return a`), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
name, err := ValidateAdapterPath(ok, dir, []string{"openai"})
|
||
if err != nil {
|
||
t.Fatalf("合法文件被误拒: %v", err)
|
||
}
|
||
if name != "my_new" {
|
||
t.Errorf("导出名 = %q, want my_new", name)
|
||
}
|
||
}
|
||
|
||
// TestErrorNamesTheConflict 验证错误信息点名冲突对象(用户要求"避免静默
|
||
// 错误"——错误必须可定位)。
|
||
func TestErrorNamesTheConflict(t *testing.T) {
|
||
err := ValidateAdapterName("openai", []string{"openai"})
|
||
if err == nil {
|
||
t.Fatal("应拒绝")
|
||
}
|
||
var reserved *ErrAdapterNameReserved
|
||
if !errors.As(err, &reserved) {
|
||
t.Fatalf("错误类型应为 ErrAdapterNameReserved, got %T", err)
|
||
}
|
||
if !strings.Contains(err.Error(), "openai") {
|
||
t.Errorf("错误信息应点名冲突对象: %v", err)
|
||
}
|
||
if !strings.Contains(err.Error(), "built-in") {
|
||
t.Errorf("错误信息应说明冲突性质: %v", err)
|
||
}
|
||
}
|