http.servefile 不安全,因不校验路径、不支持动态文件名、404无标准错误页且无法插入业务逻辑;安全方案需先 filepath.clean() 归一化,再 filepath.join() 构造绝对路径,最后 strings.hasprefix() 校验越界。

为什么不能直接用 http.ServeFile 做业务下载
它不校验路径来源,用户传 ../../etc/passwd 就能读任意文件;不支持动态文件名控制,浏览器下载的文件名完全取决于 URL 路径最后一段;返回 404 时默认吐空响应而非标准错误页,不利于前端判断;也无法插入鉴权、日志、限速等业务逻辑。
如何安全拼接并校验客户端传入的文件名
核心是“先净化、再比对、后限定”。不要只靠 strings.Contains(filename, "..") 这类简单过滤——/./、%2e%2e、空字节都可能绕过。
- 用
filepath.Clean()归一化路径,消除冗余符号 - 用
filepath.Join(rootDir, cleanName)构造绝对路径 - 用
strings.HasPrefix(absPath, rootDir)确保不越界(注意:必须是绝对路径比对) - 若允许中文名,需在
Content-Disposition中用filename*=UTF-8''编码,否则 Chrome 会截断或乱码
大文件下载必须流式写入,别碰 os.ReadFile
os.ReadFile 会把整个文件加载进内存,下个 300MB 的 Excel 就可能触发 OOM。生产环境必须走流式通道。
- 用
os.Open打开文件,立刻defer f.Close() - 用
io.Copy(w, f)直接写入ResponseWriter,不经过中间 buffer - 如需缓冲提升吞吐,可包一层
bufio.NewReader(f),但非必需 - 务必检查
resp.StatusCode再读 body,否则 404/502 的错误页也会被写进下载文件
表单 POST 触发下载时容易返回 HTML 源码
常见原因是没设对响应头,导致浏览器把二进制响应当文本渲染。关键就两行头:
-
w.Header().Set("Content-Type", "application/octet-stream")—— 避免 MIME 探测干扰 -
w.Header().Set("Content-Disposition", `attachment; filename="report.pdf"`)—— 英文名直接写;中文名用filename*=UTF-8''+url.PathEscape() - 别在 handler 里调
template.Execute或写 HTML,POST 下载必须是纯流式响应 - 如果前端用
<form target="_blank"></form>,注意某些浏览器会拦截弹窗,推荐用 JS 创建临时 link 触发下载
最易被忽略的是:路径校验必须在 os.Open 之前完成,且 filepath.Clean 和 strings.HasPrefix 必须配合使用;Content-Disposition 的编码格式一旦写错,中文名在 Firefox 和 Edge 表现不一致;还有就是 defer f.Close() 必须紧跟 os.Open,漏掉就会句柄泄漏。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











