From 2aa65de91152899f7452a6d54bc072367707fa29 Mon Sep 17 00:00:00 2001 From: "haibo.chen" <495810242@qq.com> Date: Wed, 15 Oct 2025 16:04:35 +0800 Subject: [PATCH] security --- main/main.go | 38 ++++++- main/main_test.go | 247 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 main/main_test.go diff --git a/main/main.go b/main/main.go index ced6d23..d7ae633 100644 --- a/main/main.go +++ b/main/main.go @@ -8,6 +8,7 @@ import ( "os" "os/signal" "path" + "path/filepath" "strconv" "strings" "syscall" @@ -95,9 +96,42 @@ func main() { return } + // 首先检查原始路径是否包含 ".." 以防止路径遍历攻击 + if strings.Contains(r.URL.Path, "..") { + slog.Warn("potential path traversal attempt detected", "path", r.URL.Path) + http.Error(w, "Invalid path", http.StatusBadRequest) + return + } + + // 清理路径 + cleanPath := path.Clean(r.URL.Path) + // 检查请求的文件是否存在 - filePath := path.Join(conf.Http.Dir, r.URL.Path) - _, err := os.Stat(filePath) + filePath := filepath.Join(conf.Http.Dir, cleanPath) + + // 确保最终路径在允许的目录内 + absDir, err := filepath.Abs(conf.Http.Dir) + if err != nil { + slog.Error("failed to get absolute path of http dir", "error", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + absFilePath, err := filepath.Abs(filePath) + if err != nil { + slog.Error("failed to get absolute path of file", "error", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + // 验证文件路径在允许的目录内 + if !strings.HasPrefix(absFilePath, absDir) { + slog.Warn("path traversal attempt blocked", "requested", r.URL.Path, "resolved", absFilePath) + http.Error(w, "Access denied", http.StatusForbidden) + return + } + + _, err = os.Stat(absFilePath) if os.IsNotExist(err) { // 如果文件不存在,返回 index.html slog.Info("file not found, redirect to index", "path", r.URL.Path) diff --git a/main/main_test.go b/main/main_test.go new file mode 100644 index 0000000..1a6aa55 --- /dev/null +++ b/main/main_test.go @@ -0,0 +1,247 @@ +package main + +import ( + "path" + "path/filepath" + "strings" + "testing" +) + +// TestPathTraversalPrevention 测试路径遍历防护 +func TestPathTraversalPrevention(t *testing.T) { + baseDir := "/var/www/html" + + tests := []struct { + name string + inputPath string + shouldFail bool + reason string + }{ + { + name: "Normal file", + inputPath: "/index.html", + shouldFail: false, + reason: "Normal file access should be allowed", + }, + { + name: "Subdirectory file", + inputPath: "/css/style.css", + shouldFail: false, + reason: "Subdirectory access should be allowed", + }, + { + name: "Deep subdirectory", + inputPath: "/js/lib/jquery.min.js", + shouldFail: false, + reason: "Deep subdirectory access should be allowed", + }, + { + name: "Parent directory traversal", + inputPath: "/../etc/passwd", + shouldFail: true, + reason: "Parent directory traversal should be blocked", + }, + { + name: "Double parent traversal", + inputPath: "/../../etc/passwd", + shouldFail: true, + reason: "Double parent traversal should be blocked", + }, + { + name: "Multiple parent traversal", + inputPath: "/../../../etc/passwd", + shouldFail: true, + reason: "Multiple parent traversal should be blocked", + }, + { + name: "Mixed path with parent", + inputPath: "/css/../../etc/passwd", + shouldFail: true, + reason: "Mixed path with parent should be blocked", + }, + { + name: "Dot slash path", + inputPath: "/./index.html", + shouldFail: false, + reason: "Dot slash should be cleaned but allowed", + }, + { + name: "Complex traversal", + inputPath: "/css/../js/../../../etc/passwd", + shouldFail: true, + reason: "Complex traversal should be blocked", + }, + { + name: "Root path", + inputPath: "/", + shouldFail: false, + reason: "Root path should be allowed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 模拟修复后的路径验证逻辑 + // 首先检查原始路径是否包含 ".." + containsDoubleDotInOriginal := strings.Contains(tt.inputPath, "..") + + // 如果原始路径包含 "..",直接阻止 + if containsDoubleDotInOriginal { + if !tt.shouldFail { + t.Errorf("%s: Path contains '..' but should be allowed: %s", tt.reason, tt.inputPath) + } + t.Logf("Input: %s, Contains '..': true, Blocked: true (early check)", tt.inputPath) + return + } + + // 清理路径 + cleanPath := path.Clean(tt.inputPath) + + // 构建文件路径 + filePath := filepath.Join(baseDir, cleanPath) + + // 获取绝对路径 + absDir, err := filepath.Abs(baseDir) + if err != nil { + t.Fatalf("Failed to get absolute path of base dir: %v", err) + } + + absFilePath, err := filepath.Abs(filePath) + if err != nil { + t.Fatalf("Failed to get absolute path of file: %v", err) + } + + // 验证路径是否在允许的目录内 + isOutsideBaseDir := !strings.HasPrefix(absFilePath, absDir) + + // 判断是否应该被阻止 + shouldBlock := isOutsideBaseDir + + if tt.shouldFail && !shouldBlock { + t.Errorf("%s: Expected path to be blocked, but it was allowed. Path: %s, Clean: %s, Abs: %s", + tt.reason, tt.inputPath, cleanPath, absFilePath) + } + + if !tt.shouldFail && shouldBlock { + t.Errorf("%s: Expected path to be allowed, but it was blocked. Path: %s, Clean: %s, Abs: %s", + tt.reason, tt.inputPath, cleanPath, absFilePath) + } + + // 额外的日志信息用于调试 + t.Logf("Input: %s, Clean: %s, Outside base: %v, Blocked: %v", + tt.inputPath, cleanPath, isOutsideBaseDir, shouldBlock) + }) + } +} + +// TestPathCleanBehavior 测试 path.Clean 的行为 +func TestPathCleanBehavior(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"/index.html", "/index.html"}, + {"/../etc/passwd", "/etc/passwd"}, + {"/./index.html", "/index.html"}, + {"/css/../index.html", "/index.html"}, + {"//double//slash", "/double/slash"}, + {"/trailing/slash/", "/trailing/slash"}, + {"/./././index.html", "/index.html"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result := path.Clean(tt.input) + if result != tt.expected { + t.Errorf("path.Clean(%q) = %q, expected %q", tt.input, result, tt.expected) + } + }) + } +} + +// TestAbsolutePathValidation 测试绝对路径验证 +func TestAbsolutePathValidation(t *testing.T) { + // 使用临时目录进行测试 + baseDir := t.TempDir() + + tests := []struct { + name string + path string + shouldFail bool + }{ + { + name: "File in base directory", + path: "index.html", + shouldFail: false, + }, + { + name: "File in subdirectory", + path: "css/style.css", + shouldFail: false, + }, + { + name: "Attempt to escape with parent", + path: "../outside.txt", + shouldFail: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cleanPath := path.Clean(tt.path) + filePath := filepath.Join(baseDir, cleanPath) + + absDir, err := filepath.Abs(baseDir) + if err != nil { + t.Fatalf("Failed to get absolute path: %v", err) + } + + absFilePath, err := filepath.Abs(filePath) + if err != nil { + t.Fatalf("Failed to get absolute file path: %v", err) + } + + isOutside := !strings.HasPrefix(absFilePath, absDir) + + if tt.shouldFail && !isOutside { + t.Errorf("Expected path to be outside base dir, but it wasn't: %s", absFilePath) + } + + if !tt.shouldFail && isOutside { + t.Errorf("Expected path to be inside base dir, but it wasn't: %s", absFilePath) + } + }) + } +} + +// BenchmarkPathValidation 性能基准测试 +func BenchmarkPathValidation(b *testing.B) { + baseDir := "/var/www/html" + testPath := "/css/style.css" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + cleanPath := path.Clean(testPath) + _ = strings.Contains(cleanPath, "..") + filePath := filepath.Join(baseDir, cleanPath) + absDir, _ := filepath.Abs(baseDir) + absFilePath, _ := filepath.Abs(filePath) + _ = strings.HasPrefix(absFilePath, absDir) + } +} + +// BenchmarkPathValidationMalicious 恶意路径的性能测试 +func BenchmarkPathValidationMalicious(b *testing.B) { + baseDir := "/var/www/html" + testPath := "/../../../etc/passwd" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + cleanPath := path.Clean(testPath) + _ = strings.Contains(cleanPath, "..") + filePath := filepath.Join(baseDir, cleanPath) + absDir, _ := filepath.Abs(baseDir) + absFilePath, _ := filepath.Abs(filePath) + _ = strings.HasPrefix(absFilePath, absDir) + } +}