security
This commit is contained in:
38
main/main.go
38
main/main.go
@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
@ -95,9 +96,42 @@ func main() {
|
|||||||
return
|
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)
|
filePath := filepath.Join(conf.Http.Dir, cleanPath)
|
||||||
_, err := os.Stat(filePath)
|
|
||||||
|
// 确保最终路径在允许的目录内
|
||||||
|
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) {
|
if os.IsNotExist(err) {
|
||||||
// 如果文件不存在,返回 index.html
|
// 如果文件不存在,返回 index.html
|
||||||
slog.Info("file not found, redirect to index", "path", r.URL.Path)
|
slog.Info("file not found, redirect to index", "path", r.URL.Path)
|
||||||
|
|||||||
247
main/main_test.go
Normal file
247
main/main_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user