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