fix(security): /uploads 目录路径遍历防护

- 替换 Static 为受控文件服务 handler (serveUploads)
- 添加 filepath.Clean 路径清理 + .. 检测
- 使用 Abs + HasPrefix 限制访问范围在上传目录内
- 添加安全响应头(CSP default-src 'none', X-Content-Type-Options nosniff)
This commit is contained in:
2026-05-08 12:28:03 +08:00
parent e49865df11
commit 61692e4c1a
4 changed files with 60 additions and 8 deletions

View File

@@ -1,6 +1,11 @@
package router
import (
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
swaggerFiles "github.com/swaggo/files"
@@ -122,9 +127,9 @@ func (r *Router) Setup() *gin.Engine {
)
}
// P0 安全修复:/uploads 目录不再公开暴露,改为需要认证后才能访问
// P0 安全修复:/uploads 目录使用受控文件服务,防止路径遍历
uploadsGroup := r.engine.Group("/uploads", r.authMiddleware.Required())
uploadsGroup.Static("", "./uploads")
uploadsGroup.GET("/*filepath", r.serveUploads)
r.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
@@ -408,3 +413,37 @@ func (r *Router) Setup() *gin.Engine {
func (r *Router) GetEngine() *gin.Engine {
return r.engine
}
// serveUploads 提供受控的上传文件访问,防止路径遍历攻击
func (r *Router) serveUploads(c *gin.Context) {
filePath := c.Param("filepath")
// 1. 清理路径,阻止路径遍历
filePath = filepath.Clean("/" + filePath)
if strings.Contains(filePath, "..") {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": 403, "message": "invalid path"})
return
}
// 2. 限制在上传目录内
fullPath := filepath.Join("./uploads", filePath)
absUploads, _ := filepath.Abs("./uploads")
absPath, _ := filepath.Abs(fullPath)
if !strings.HasPrefix(absPath, absUploads) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": 403, "message": "access denied"})
return
}
// 3. 检查文件存在
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
c.AbortWithStatus(http.StatusNotFound)
return
}
// 4. 设置安全响应头(禁止浏览器执行)
c.Header("Content-Security-Policy", "default-src 'none'")
c.Header("X-Content-Type-Options", "nosniff")
// 5. 提供文件
c.File(fullPath)
}