Files
go_blog/main_test.go
T
kevin f7870e8557 fix: 修复 P0 复审漏洞 #18 #19(数据库泄露 + SQL 注入)
- #18 /uploads 改为白名单子目录挂载(attachments/avatars/logos + 配置的
  存储目录),存储根不再整体暴露,blog.db 不可被下载;禁用目录列表,
  拒绝 .. 穿越与反斜杠
- #19 admin 用户管理三个 handler 的路由参数先解析为数值(uintFormID),
  非数值直接 302,消除 GORM First() 字符串条件 SQL 注入
- 新增回归测试 main_test.go(4 用例)与 TestAdminUserRoutesRejectNonNumericIDs
  (已变异验证:旧代码下注入用例失败)
- SECURITY_TODO.md 勾选 #18/#19 并更新执行顺序
2026-08-27 17:08:22 +08:00

130 lines
4.1 KiB
Go

package main
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/gin-gonic/gin"
)
// newUploadsRouter builds a router with the production upload routes over a
// temp storage root that mirrors the real layout: the SQLite database file
// lives in the root itself, uploads live in subdirectories.
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")
}
// The database file in the storage root must not be downloadable.
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)
}
// No directory listing anywhere.
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)
}
}
// Traversal attempts must not escape the subdirectory.
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)
}
}
// Files in the whitelisted subdirectories are still served.
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)
}
// The default dir stays mounted for backward compatibility.
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) {
// A storage dir equal to a known dir must not panic on duplicate routes.
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)
}
}