Files

100 lines
3.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
// 沙箱是本地执行的安全命门:路径清洗/软链逃逸/越界读全都要拒——这里逐项钉死。
func newSandbox(t *testing.T) string {
t.Helper()
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "a.txt"), []byte("hello"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(root, "sub"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "sub", "b.txt"), []byte("world"), 0o644); err != nil {
t.Fatal(err)
}
return root
}
func TestResolveInRootBlocksEscape(t *testing.T) {
root := newSandbox(t)
// 断言基准要用软链解析后的 rootmacOS 的 TempDir 在 /var(→/private/var 软链)下,
// resolveInRoot 返回的是解析后的绝对路径,拿未解析 root 做前缀比较会误报。
rootR, err := filepath.EvalSymlinks(root)
if err != nil {
rootR = root
}
for _, rel := range []string{"..", "../..", "../../etc/passwd", "sub/../../outside", "/etc/passwd"} {
p, err := resolveInRoot(root, rel)
// 前置 "/"+Clean 把绝对路径/.. 都钉回 root 下(如 root/etc/passwd,不算逃逸);
// 无论哪种形式,成功解析的结果都必须仍在 root(解析后)之内。
if err == nil && p != rootR && !strings.HasPrefix(p, rootR+string(filepath.Separator)) {
t.Fatalf("路径 %q 逃出了沙箱: %s", rel, p)
}
}
}
func TestResolveInRootBlocksSymlinkEscape(t *testing.T) {
root := newSandbox(t)
outside := t.TempDir()
if err := os.WriteFile(filepath.Join(outside, "secret.txt"), []byte("secret"), 0o644); err != nil {
t.Fatal(err)
}
link := filepath.Join(root, "evil")
if err := os.Symlink(outside, link); err != nil {
t.Skip("无法创建软链,跳过")
}
if _, err := resolveInRoot(root, "evil/secret.txt"); err == nil {
t.Fatal("软链逃逸未被拦截")
}
}
func TestExecLocalListAndRead(t *testing.T) {
root := newSandbox(t)
// list_dir 根目录
resp := execLocal(root, &runnerReq{ID: "1", Tool: "local_list_dir", Args: map[string]any{"path": ""}})
if !resp.OK {
t.Fatalf("list_dir 失败: %s", resp.Error)
}
var listing struct {
Entries []struct {
Name string `json:"name"`
Dir bool `json:"dir"`
} `json:"entries"`
}
if err := json.Unmarshal([]byte(resp.Content), &listing); err != nil {
t.Fatal(err)
}
if len(listing.Entries) != 2 {
t.Fatalf("期望 2 项,得 %d", len(listing.Entries))
}
// read_file 子目录文件
resp = execLocal(root, &runnerReq{ID: "2", Tool: "local_read_file", Args: map[string]any{"path": "sub/b.txt"}})
if !resp.OK || resp.Content != "world" {
t.Fatalf("read_file 失败: ok=%v content=%q err=%s", resp.OK, resp.Content, resp.Error)
}
// read_file 越界必须拒
resp = execLocal(root, &runnerReq{ID: "3", Tool: "local_read_file", Args: map[string]any{"path": "../outside.txt"}})
if resp.OK {
t.Fatal("越界读未被拦截")
}
// 未知工具(写/exec 都不在只读版里)必须拒
resp = execLocal(root, &runnerReq{ID: "4", Tool: "local_write_file", Args: map[string]any{"path": "a.txt"}})
if resp.OK {
t.Fatal("未注册操作未被拦截")
}
}