mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-10-31 02:01:44 +08:00
177 lines
6.4 KiB
Go
177 lines
6.4 KiB
Go
package utils
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"SamWaf/global"
|
|
)
|
|
|
|
// 批量任务本地来源的路径收口用例。
|
|
// 收口后:只能读「内置 data/import + config.yml 声明的目录」,且软链接不能把读取引到目录之外。
|
|
|
|
// setBatchImportBase 把程序目录指到临时目录,并按需设置 config 允许清单,用例结束还原。
|
|
func setBatchImportBase(t *testing.T, base, allowedDirs string) {
|
|
t.Helper()
|
|
oldBase := batchImportBaseDir
|
|
batchImportBaseDir = func() string { return base }
|
|
oldDirs := global.GCONFIG_BATCH_IMPORT_ALLOWED_DIRS
|
|
global.GCONFIG_BATCH_IMPORT_ALLOWED_DIRS = allowedDirs
|
|
t.Cleanup(func() {
|
|
batchImportBaseDir = oldBase
|
|
global.GCONFIG_BATCH_IMPORT_ALLOWED_DIRS = oldDirs
|
|
})
|
|
}
|
|
|
|
// mustWrite 在指定目录下写一个文件并返回其路径
|
|
func mustWrite(t *testing.T, dir, name, content string) string {
|
|
t.Helper()
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatalf("建目录失败: %v", err)
|
|
}
|
|
p := filepath.Join(dir, name)
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("写文件失败: %v", err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestValidateBatchLocalPath_允许目录内正常读(t *testing.T) {
|
|
base := filepath.Join(t.TempDir(), "samwaf")
|
|
setBatchImportBase(t, base, "")
|
|
importDir := filepath.Join(base, "data", "import")
|
|
p := mustWrite(t, importDir, "ip.txt", "1.2.3.4\n")
|
|
|
|
got, err := ValidateBatchLocalPath(p)
|
|
if err != nil {
|
|
t.Fatalf("内置导入目录内的文件应放行,实际被拒: %v", err)
|
|
}
|
|
if got == "" {
|
|
t.Fatal("返回路径不应为空")
|
|
}
|
|
|
|
// 只填文件名时按内置导入目录解析,方便用户使用
|
|
if _, err = ValidateBatchLocalPath("ip.txt"); err != nil {
|
|
t.Fatalf("相对文件名应按内置导入目录解析,实际被拒: %v", err)
|
|
}
|
|
// 子目录同样在允许范围内
|
|
sub := mustWrite(t, filepath.Join(importDir, "sub"), "ip2.txt", "5.6.7.8\n")
|
|
if _, err = ValidateBatchLocalPath(sub); err != nil {
|
|
t.Fatalf("允许目录的子目录应放行,实际被拒: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateBatchLocalPath_越界路径被拒(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
base := filepath.Join(tmp, "samwaf")
|
|
setBatchImportBase(t, base, "")
|
|
importDir := filepath.Join(base, "data", "import")
|
|
if err := os.MkdirAll(importDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 允许目录之外真实存在的一个文件:证明"被拒"不是因为文件不存在
|
|
outside := mustWrite(t, filepath.Join(tmp, "secret"), "shadow.txt", "root:x:0:0")
|
|
|
|
cases := []struct {
|
|
name string
|
|
path string
|
|
}{
|
|
{"空字符串", ""},
|
|
{"全是空格", " "},
|
|
{"含换行", filepath.Join(importDir, "a\nb.txt")},
|
|
{"含NUL", filepath.Join(importDir, "a\x00b.txt")},
|
|
{"以分隔符结尾", importDir + string(filepath.Separator)},
|
|
{"允许目录之外的绝对路径", outside},
|
|
{"相对路径上跳逃逸", filepath.Join("..", "..", "secret", "shadow.txt")},
|
|
{"绝对路径带上跳段", filepath.Join(importDir, "..", "..", "..", "secret", "shadow.txt")},
|
|
{"允许根目录本身", importDir},
|
|
{"目录不是普通文件", filepath.Join(importDir, ".")},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
if got, err := ValidateBatchLocalPath(c.path); err == nil {
|
|
t.Fatalf("期望拒绝 %q,实际通过并返回 %q", c.path, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateBatchLocalPath_前缀相同的兄弟目录被拒(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
base := filepath.Join(tmp, "samwaf")
|
|
setBatchImportBase(t, base, "")
|
|
// data/import_evil 与 data/import 共享字符串前缀,只做 HasPrefix 会误放行
|
|
evil := mustWrite(t, filepath.Join(base, "data", "import_evil"), "x.txt", "1.2.3.4")
|
|
if got, err := ValidateBatchLocalPath(evil); err == nil {
|
|
t.Fatalf("同前缀兄弟目录必须被拒,实际通过并返回 %q", got)
|
|
}
|
|
}
|
|
|
|
func TestValidateBatchLocalPath_config声明的目录被放行(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
base := filepath.Join(tmp, "samwaf")
|
|
extra := filepath.Join(tmp, "mirror")
|
|
setBatchImportBase(t, base, extra)
|
|
p := mustWrite(t, extra, "list.txt", "1.2.3.4\n")
|
|
|
|
if _, err := ValidateBatchLocalPath(p); err != nil {
|
|
t.Fatalf("config 声明的目录应放行,实际被拒: %v", err)
|
|
}
|
|
|
|
// 清单清空后立刻恢复 fail-closed
|
|
global.GCONFIG_BATCH_IMPORT_ALLOWED_DIRS = ""
|
|
if got, err := ValidateBatchLocalPath(p); err == nil {
|
|
t.Fatalf("清空允许清单后必须拒绝,实际通过并返回 %q", got)
|
|
}
|
|
}
|
|
|
|
func TestValidateBatchLocalPath_软链接逃逸被拒(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Windows 建软链接需要额外权限,跳过")
|
|
}
|
|
tmp := t.TempDir()
|
|
base := filepath.Join(tmp, "samwaf")
|
|
setBatchImportBase(t, base, "")
|
|
importDir := filepath.Join(base, "data", "import")
|
|
if err := os.MkdirAll(importDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
outside := mustWrite(t, filepath.Join(tmp, "secret"), "shadow.txt", "root:x:0:0")
|
|
|
|
// 终点软链接:允许目录内的文件名,指向目录之外
|
|
link := filepath.Join(importDir, "innocent.txt")
|
|
if err := os.Symlink(outside, link); err != nil {
|
|
t.Skipf("当前环境无法创建软链接: %v", err)
|
|
}
|
|
if got, err := ValidateBatchLocalPath(link); err == nil {
|
|
t.Fatalf("指向允许目录之外的软链接必须被拒,实际通过并返回 %q", got)
|
|
}
|
|
|
|
// 中间目录软链接:allowed/linkdir -> /tmp/secret,再读 allowed/linkdir/shadow.txt
|
|
linkDir := filepath.Join(importDir, "linkdir")
|
|
if err := os.Symlink(filepath.Join(tmp, "secret"), linkDir); err != nil {
|
|
t.Skipf("当前环境无法创建软链接: %v", err)
|
|
}
|
|
if got, err := ValidateBatchLocalPath(filepath.Join(linkDir, "shadow.txt")); err == nil {
|
|
t.Fatalf("经软链接目录读到允许目录之外必须被拒,实际通过并返回 %q", got)
|
|
}
|
|
}
|
|
|
|
func TestPrecheckBatchLocalPath_文件未就位也放行但越界仍拒(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
base := filepath.Join(tmp, "samwaf")
|
|
setBatchImportBase(t, base, "")
|
|
importDir := filepath.Join(base, "data", "import")
|
|
|
|
// 保存任务时文件还没放上来:路径策略合法即放行
|
|
if err := PrecheckBatchLocalPath(filepath.Join(importDir, "not-yet.txt")); err != nil {
|
|
t.Fatalf("允许目录内的未创建文件在保存阶段应放行,实际被拒: %v", err)
|
|
}
|
|
// 越界路径无论文件在不在都必须拒
|
|
if err := PrecheckBatchLocalPath(filepath.Join(tmp, "elsewhere", "x.txt")); err == nil {
|
|
t.Fatal("越界路径在保存阶段就必须被拒")
|
|
}
|
|
}
|