Skip to content

Commit 2aa65de

Browse files
security
1 parent 35de09a commit 2aa65de

2 files changed

Lines changed: 283 additions & 2 deletions

File tree

main/main.go

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"os/signal"
1010
"path"
11+
"path/filepath"
1112
"strconv"
1213
"strings"
1314
"syscall"
@@ -95,9 +96,42 @@ func main() {
9596
return
9697
}
9798

99+
// 首先检查原始路径是否包含 ".." 以防止路径遍历攻击
100+
if strings.Contains(r.URL.Path, "..") {
101+
slog.Warn("potential path traversal attempt detected", "path", r.URL.Path)
102+
http.Error(w, "Invalid path", http.StatusBadRequest)
103+
return
104+
}
105+
106+
// 清理路径
107+
cleanPath := path.Clean(r.URL.Path)
108+
98109
// 检查请求的文件是否存在
99-
filePath := path.Join(conf.Http.Dir, r.URL.Path)
100-
_, err := os.Stat(filePath)
110+
filePath := filepath.Join(conf.Http.Dir, cleanPath)
111+
112+
// 确保最终路径在允许的目录内
113+
absDir, err := filepath.Abs(conf.Http.Dir)
114+
if err != nil {
115+
slog.Error("failed to get absolute path of http dir", "error", err)
116+
http.Error(w, "Internal server error", http.StatusInternalServerError)
117+
return
118+
}
119+
120+
absFilePath, err := filepath.Abs(filePath)
121+
if err != nil {
122+
slog.Error("failed to get absolute path of file", "error", err)
123+
http.Error(w, "Internal server error", http.StatusInternalServerError)
124+
return
125+
}
126+
127+
// 验证文件路径在允许的目录内
128+
if !strings.HasPrefix(absFilePath, absDir) {
129+
slog.Warn("path traversal attempt blocked", "requested", r.URL.Path, "resolved", absFilePath)
130+
http.Error(w, "Access denied", http.StatusForbidden)
131+
return
132+
}
133+
134+
_, err = os.Stat(absFilePath)
101135
if os.IsNotExist(err) {
102136
// 如果文件不存在,返回 index.html
103137
slog.Info("file not found, redirect to index", "path", r.URL.Path)

main/main_test.go

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package main
2+
3+
import (
4+
"path"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
)
9+
10+
// TestPathTraversalPrevention 测试路径遍历防护
11+
func TestPathTraversalPrevention(t *testing.T) {
12+
baseDir := "/var/www/html"
13+
14+
tests := []struct {
15+
name string
16+
inputPath string
17+
shouldFail bool
18+
reason string
19+
}{
20+
{
21+
name: "Normal file",
22+
inputPath: "/index.html",
23+
shouldFail: false,
24+
reason: "Normal file access should be allowed",
25+
},
26+
{
27+
name: "Subdirectory file",
28+
inputPath: "/css/style.css",
29+
shouldFail: false,
30+
reason: "Subdirectory access should be allowed",
31+
},
32+
{
33+
name: "Deep subdirectory",
34+
inputPath: "/js/lib/jquery.min.js",
35+
shouldFail: false,
36+
reason: "Deep subdirectory access should be allowed",
37+
},
38+
{
39+
name: "Parent directory traversal",
40+
inputPath: "/../etc/passwd",
41+
shouldFail: true,
42+
reason: "Parent directory traversal should be blocked",
43+
},
44+
{
45+
name: "Double parent traversal",
46+
inputPath: "/../../etc/passwd",
47+
shouldFail: true,
48+
reason: "Double parent traversal should be blocked",
49+
},
50+
{
51+
name: "Multiple parent traversal",
52+
inputPath: "/../../../etc/passwd",
53+
shouldFail: true,
54+
reason: "Multiple parent traversal should be blocked",
55+
},
56+
{
57+
name: "Mixed path with parent",
58+
inputPath: "/css/../../etc/passwd",
59+
shouldFail: true,
60+
reason: "Mixed path with parent should be blocked",
61+
},
62+
{
63+
name: "Dot slash path",
64+
inputPath: "/./index.html",
65+
shouldFail: false,
66+
reason: "Dot slash should be cleaned but allowed",
67+
},
68+
{
69+
name: "Complex traversal",
70+
inputPath: "/css/../js/../../../etc/passwd",
71+
shouldFail: true,
72+
reason: "Complex traversal should be blocked",
73+
},
74+
{
75+
name: "Root path",
76+
inputPath: "/",
77+
shouldFail: false,
78+
reason: "Root path should be allowed",
79+
},
80+
}
81+
82+
for _, tt := range tests {
83+
t.Run(tt.name, func(t *testing.T) {
84+
// 模拟修复后的路径验证逻辑
85+
// 首先检查原始路径是否包含 ".."
86+
containsDoubleDotInOriginal := strings.Contains(tt.inputPath, "..")
87+
88+
// 如果原始路径包含 "..",直接阻止
89+
if containsDoubleDotInOriginal {
90+
if !tt.shouldFail {
91+
t.Errorf("%s: Path contains '..' but should be allowed: %s", tt.reason, tt.inputPath)
92+
}
93+
t.Logf("Input: %s, Contains '..': true, Blocked: true (early check)", tt.inputPath)
94+
return
95+
}
96+
97+
// 清理路径
98+
cleanPath := path.Clean(tt.inputPath)
99+
100+
// 构建文件路径
101+
filePath := filepath.Join(baseDir, cleanPath)
102+
103+
// 获取绝对路径
104+
absDir, err := filepath.Abs(baseDir)
105+
if err != nil {
106+
t.Fatalf("Failed to get absolute path of base dir: %v", err)
107+
}
108+
109+
absFilePath, err := filepath.Abs(filePath)
110+
if err != nil {
111+
t.Fatalf("Failed to get absolute path of file: %v", err)
112+
}
113+
114+
// 验证路径是否在允许的目录内
115+
isOutsideBaseDir := !strings.HasPrefix(absFilePath, absDir)
116+
117+
// 判断是否应该被阻止
118+
shouldBlock := isOutsideBaseDir
119+
120+
if tt.shouldFail && !shouldBlock {
121+
t.Errorf("%s: Expected path to be blocked, but it was allowed. Path: %s, Clean: %s, Abs: %s",
122+
tt.reason, tt.inputPath, cleanPath, absFilePath)
123+
}
124+
125+
if !tt.shouldFail && shouldBlock {
126+
t.Errorf("%s: Expected path to be allowed, but it was blocked. Path: %s, Clean: %s, Abs: %s",
127+
tt.reason, tt.inputPath, cleanPath, absFilePath)
128+
}
129+
130+
// 额外的日志信息用于调试
131+
t.Logf("Input: %s, Clean: %s, Outside base: %v, Blocked: %v",
132+
tt.inputPath, cleanPath, isOutsideBaseDir, shouldBlock)
133+
})
134+
}
135+
}
136+
137+
// TestPathCleanBehavior 测试 path.Clean 的行为
138+
func TestPathCleanBehavior(t *testing.T) {
139+
tests := []struct {
140+
input string
141+
expected string
142+
}{
143+
{"/index.html", "/index.html"},
144+
{"/../etc/passwd", "/etc/passwd"},
145+
{"/./index.html", "/index.html"},
146+
{"/css/../index.html", "/index.html"},
147+
{"//double//slash", "/double/slash"},
148+
{"/trailing/slash/", "/trailing/slash"},
149+
{"/./././index.html", "/index.html"},
150+
}
151+
152+
for _, tt := range tests {
153+
t.Run(tt.input, func(t *testing.T) {
154+
result := path.Clean(tt.input)
155+
if result != tt.expected {
156+
t.Errorf("path.Clean(%q) = %q, expected %q", tt.input, result, tt.expected)
157+
}
158+
})
159+
}
160+
}
161+
162+
// TestAbsolutePathValidation 测试绝对路径验证
163+
func TestAbsolutePathValidation(t *testing.T) {
164+
// 使用临时目录进行测试
165+
baseDir := t.TempDir()
166+
167+
tests := []struct {
168+
name string
169+
path string
170+
shouldFail bool
171+
}{
172+
{
173+
name: "File in base directory",
174+
path: "index.html",
175+
shouldFail: false,
176+
},
177+
{
178+
name: "File in subdirectory",
179+
path: "css/style.css",
180+
shouldFail: false,
181+
},
182+
{
183+
name: "Attempt to escape with parent",
184+
path: "../outside.txt",
185+
shouldFail: true,
186+
},
187+
}
188+
189+
for _, tt := range tests {
190+
t.Run(tt.name, func(t *testing.T) {
191+
cleanPath := path.Clean(tt.path)
192+
filePath := filepath.Join(baseDir, cleanPath)
193+
194+
absDir, err := filepath.Abs(baseDir)
195+
if err != nil {
196+
t.Fatalf("Failed to get absolute path: %v", err)
197+
}
198+
199+
absFilePath, err := filepath.Abs(filePath)
200+
if err != nil {
201+
t.Fatalf("Failed to get absolute file path: %v", err)
202+
}
203+
204+
isOutside := !strings.HasPrefix(absFilePath, absDir)
205+
206+
if tt.shouldFail && !isOutside {
207+
t.Errorf("Expected path to be outside base dir, but it wasn't: %s", absFilePath)
208+
}
209+
210+
if !tt.shouldFail && isOutside {
211+
t.Errorf("Expected path to be inside base dir, but it wasn't: %s", absFilePath)
212+
}
213+
})
214+
}
215+
}
216+
217+
// BenchmarkPathValidation 性能基准测试
218+
func BenchmarkPathValidation(b *testing.B) {
219+
baseDir := "/var/www/html"
220+
testPath := "/css/style.css"
221+
222+
b.ResetTimer()
223+
for i := 0; i < b.N; i++ {
224+
cleanPath := path.Clean(testPath)
225+
_ = strings.Contains(cleanPath, "..")
226+
filePath := filepath.Join(baseDir, cleanPath)
227+
absDir, _ := filepath.Abs(baseDir)
228+
absFilePath, _ := filepath.Abs(filePath)
229+
_ = strings.HasPrefix(absFilePath, absDir)
230+
}
231+
}
232+
233+
// BenchmarkPathValidationMalicious 恶意路径的性能测试
234+
func BenchmarkPathValidationMalicious(b *testing.B) {
235+
baseDir := "/var/www/html"
236+
testPath := "/../../../etc/passwd"
237+
238+
b.ResetTimer()
239+
for i := 0; i < b.N; i++ {
240+
cleanPath := path.Clean(testPath)
241+
_ = strings.Contains(cleanPath, "..")
242+
filePath := filepath.Join(baseDir, cleanPath)
243+
absDir, _ := filepath.Abs(baseDir)
244+
absFilePath, _ := filepath.Abs(filePath)
245+
_ = strings.HasPrefix(absFilePath, absDir)
246+
}
247+
}

0 commit comments

Comments
 (0)