生产环境禁用http.ServeFile下载文件,因其存在路径暴露、目录遍历、无法设Content-Disposition等风险;应使用http.ServeContent配合路径校验、安全目录检查、正确modtime和Content-Disposition设置实现安全可控的文件下载。

go中用http.ServeFile直接下载文件有风险
它会暴露文件系统路径,且不支持自定义响应头(如Content-Disposition),还可能触发目录遍历漏洞。生产环境必须避免裸用。
- 若路径来自用户输入(如
/download?file=../etc/passwd),http.ServeFile不做校验就可能读取任意文件 - 无法设置
Content-Type为application/octet-stream,浏览器可能尝试渲染而非下载 - 不支持断点续传、大文件流式处理,容易OOM
推荐方式:用io.copy + 自定义http.ResponseWriter
手动控制响应头和流式写入,兼顾安全与可控性。核心是设置Content-Disposition并禁用缓存。
func downloadHandler(w http.ResponseWriter, r *http.Request) { filename := r.URL.Query().Get("file") // 1. 白名单校验或路径净化(关键!) if !isValidFilename(filename) { http.Error(w, "Invalid file", http.StatusForbidden) return } filepath := path.Join("/safe/download/dir", filename) // 2. 检查文件是否存在且在允许目录内 if !isInSafeDir(filepath, "/safe/download/dir") { http.Error(w, "access denied", http.StatusForbidden) return } // 3. 设置响应头 w.Header().Set("Content-Description", "File Transfer") w.Header().Set("Content-Transfer-Encoding", "binary") w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`) w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Expires", "0") w.Header().Set("Cache-Control", "must-revalidate") w.Header().Set("Pragma", "public") // 4. 流式传输,避免内存堆积 file, err := os.Open(filepath) if err != nil { http.Error(w, "File not found", http.StatusNotFound) return } defer file.Close() http.ServeContent(w, r, filename, time.Now(), file) }
http.ServeContent比io.Copy更稳妥
它自动处理If-Range、Range请求,支持断点续传,还带ETag和Last-Modified校验。直接用io.Copy会丢掉这些能力。
-
http.ServeContent第四个参数是modtime,务必传真实文件修改时间,否则协商缓存失效 - 第三个参数(
name)影响Content-Disposition生成逻辑,建议显式传原始文件名 - 底层仍调用
io.Copy,但封装了HTTP/1.1分块、状态码判断等细节
大文件或需权限控制时,别用os.Open直读
应结合http.Range解析和io.Seeker做偏移读取,否则1GB文件会阻塞goroutine并吃光内存。
- 用
file.Seek(offset, io.SeekStart)跳转到Range起始位置 - 用
io.LimitReader(file, Length)限制读取字节数,防止超范围 - 权限检查必须在
Seek前完成,避免绕过校验 - 对敏感文件,考虑加临时Token签名(如
/dl/{token}/{filename}),过期即失效
实际部署时最容易忽略的是路径净化和modtime传参——前者导致越权访问,后者让缓存永远不生效。