- 48 个 Go 文件所有注释(行注释/块注释/行尾注释,含 _test.go)翻译为中文 - 保留技术标识符:SECURITY_TODO(n)、unsafe-inline、sqlite/mysql、路由参数等 - 代码、字符串字面量、日志消息保持英文原文,零逻辑改动 - go build/vet 通过,go test -count=1 ./... 全绿
130 lines
4.1 KiB
Go
130 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// newUploadsRouter 在模拟真实布局的临时存储根目录上构建
|
|
// 使用生产上传路由的路由器:SQLite 数据库文件位于根目录中,
|
|
// 上传文件位于子目录中。
|
|
func newUploadsRouter(t *testing.T, storageDir string) (*gin.Engine, string) {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
root := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(root, "blog.db"), []byte("fake sqlite"), 0644); err != nil {
|
|
t.Fatalf("seed blog.db: %v", err)
|
|
}
|
|
|
|
r := gin.New()
|
|
registerUploadRoutes(r.Group("/uploads"), root, storageDir)
|
|
return r, root
|
|
}
|
|
|
|
func doGet(t *testing.T, r *gin.Engine, path string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
|
|
return w
|
|
}
|
|
|
|
func seedUploadFile(t *testing.T, root, sub, name string) {
|
|
t.Helper()
|
|
dir := filepath.Join(root, sub)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", dir, err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte("content"), 0644); err != nil {
|
|
t.Fatalf("seed %s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
func TestUploadsWhitelistHidesStorageRoot(t *testing.T) {
|
|
r, root := newUploadsRouter(t, "")
|
|
for _, sub := range []string{"attachments", "avatars", "logos"} {
|
|
seedUploadFile(t, root, sub, "file.txt")
|
|
}
|
|
|
|
// 存储根目录中的数据库文件必须不可下载。
|
|
if w := doGet(t, r, "/uploads/blog.db"); w.Code != http.StatusNotFound {
|
|
t.Fatalf("GET /uploads/blog.db = %d, want 404 (database leak)", w.Code)
|
|
}
|
|
|
|
// 任何地方都不允许目录列表。
|
|
for _, p := range []string{
|
|
"/uploads", "/uploads/",
|
|
"/uploads/attachments/", "/uploads/avatars/", "/uploads/logos/",
|
|
} {
|
|
if w := doGet(t, r, p); w.Code != http.StatusNotFound {
|
|
t.Fatalf("GET %s = %d, want 404 (no directory listing)", p, w.Code)
|
|
}
|
|
}
|
|
|
|
// 路径穿越尝试不得逃逸出子目录。
|
|
for _, p := range []string{
|
|
"/uploads/attachments/../blog.db",
|
|
"/uploads/attachments/..%2f..%2fblog.db",
|
|
"/uploads/attachments/%2e%2e/blog.db",
|
|
} {
|
|
if w := doGet(t, r, p); w.Code == http.StatusOK {
|
|
t.Fatalf("GET %s = %d, want non-200 (traversal)", p, w.Code)
|
|
}
|
|
}
|
|
|
|
// 白名单子目录中的文件仍然可以访问。
|
|
for _, p := range []string{
|
|
"/uploads/attachments/file.txt",
|
|
"/uploads/avatars/file.txt",
|
|
"/uploads/logos/file.txt",
|
|
} {
|
|
if w := doGet(t, r, p); w.Code != http.StatusOK {
|
|
t.Fatalf("GET %s = %d, want 200", p, w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUploadsWhitelistCustomStorageDir(t *testing.T) {
|
|
r, root := newUploadsRouter(t, "files")
|
|
seedUploadFile(t, root, "files", "a.bin")
|
|
|
|
if w := doGet(t, r, "/uploads/files/a.bin"); w.Code != http.StatusOK {
|
|
t.Fatalf("GET /uploads/files/a.bin = %d, want 200", w.Code)
|
|
}
|
|
// 默认目录保持挂载以向后兼容。
|
|
seedUploadFile(t, root, "attachments", "old.txt")
|
|
if w := doGet(t, r, "/uploads/attachments/old.txt"); w.Code != http.StatusOK {
|
|
t.Fatalf("GET /uploads/attachments/old.txt = %d, want 200", w.Code)
|
|
}
|
|
if w := doGet(t, r, "/uploads/blog.db"); w.Code != http.StatusNotFound {
|
|
t.Fatalf("GET /uploads/blog.db = %d, want 404", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestUploadsWhitelistUnsafeStorageDirFallsBack(t *testing.T) {
|
|
for _, dir := range []string{"../evil", "/etc", "..", "a/../../b", "."} {
|
|
r, root := newUploadsRouter(t, dir)
|
|
seedUploadFile(t, root, "attachments", "file.txt")
|
|
if w := doGet(t, r, "/uploads/attachments/file.txt"); w.Code != http.StatusOK {
|
|
t.Fatalf("storage dir %q: fallback mount broken: %d", dir, w.Code)
|
|
}
|
|
if w := doGet(t, r, "/uploads/blog.db"); w.Code != http.StatusNotFound {
|
|
t.Fatalf("storage dir %q: /uploads/blog.db = %d, want 404", dir, w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUploadsWhitelistStorageDirDedup(t *testing.T) {
|
|
// 与已知目录相同的存储目录不能因重复路由而 panic。
|
|
r, root := newUploadsRouter(t, "avatars")
|
|
seedUploadFile(t, root, "avatars", "me.jpg")
|
|
if w := doGet(t, r, "/uploads/avatars/me.jpg"); w.Code != http.StatusOK {
|
|
t.Fatalf("GET /uploads/avatars/me.jpg = %d, want 200", w.Code)
|
|
}
|
|
}
|