不安全,因http.ServeFile不净化路径,用户输入恶意路径如../../etc/passwd可导致目录穿越;应使用http.FileServer配合StripPrefix限定根目录,并校验clean后路径是否在白名单内。

用 http.ServeFile 直接预览静态文件是否安全?
不安全,尤其当路径来自用户输入时。http.ServeFile 不做路径净化,攻击者传入 ../../etc/passwd 就可能读取系统文件。它只适合服务已知固定路径(如 ./public/index.html),且必须确保该路径在白名单目录内。
正确做法是用 http.FileServer 配合 http.StripPrefix,并限定根目录:
fs := http.FileServer(http.Dir("./uploads/"))
http.Handle("/preview/", http.StripPrefix("/preview/", fs))
这样所有请求都会被限制在 ./uploads/ 下,/preview/../../secret.txt 会被自动拒绝(返回 404)。
如何判断文件能否直接浏览器预览?
关键看 MIME 类型是否被浏览器支持,而不是文件扩展名。Golang 提供 net/http.DetectContentType,但它只读前 512 字节,对小文件可靠,对大文件或压缩包无效;更稳妥的是结合扩展名 + mime.TypeByExtension:
立即学习“go语言免费学习笔记(深入)”;
-
mime.TypeByExtension(".pdf")返回application/pdf -
mime.TypeByExtension(".jpg")返回image/jpeg - 未知扩展名时 fallback 到
application/octet-stream(强制下载)
注意:不要依赖客户端传来的 Content-Type,它可被伪造;也不要仅靠 DetectContentType 处理 ZIP、DOCX 等复合格式——它们头部特征易误判。
预览 PDF / 图片 / 文本时怎么控制响应头?
浏览器是否内嵌显示,取决于 Content-Type 和 Content-Disposition。例如:
- PDF 想在线打开:设
Content-Type: application/pdf,不设Content-Disposition - 文本想强制下载:设
Content-Type: text/plain+Content-Disposition: attachment; filename="log.txt" - 图片想防止右键另存为:做不到,但可加
X-Content-Type-Options: nosniff防 MIME 嗅探
示例代码片段:
func previewHandler(w http.ResponseWriter, r *http.Request) {
path := filepath.Join("./uploads/", r.URL.Query().Get("file"))
if !strings.HasPrefix(filepath.Clean(path), "./uploads/") {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
ext := strings.ToLower(filepath.Ext(path))
mime := mime.TypeByExtension(ext)
if mime == "" {
mime = "application/octet-stream"
}
w.Header().Set("Content-Type", mime)
if mime == "text/plain" || mime == "text/markdown" {
w.Header().Set("Content-Disposition", "inline")
}
http.ServeFile(w, r, path)
}
大文件(>100MB)预览卡顿怎么办?
直接 http.ServeFile 会把整个文件读进内存或阻塞 goroutine,导致并发下降。应改用流式响应 + 分块读取:
- 用
os.Open打开文件,再用io.CopyN或io.Copy写到w - 设置
Content-Length(需提前stat获取大小)以支持断点续传和进度条 - 对视频/音频加
Accept-Ranges: bytes和处理Range请求头(否则无法拖动进度条)
简单流式写法:
f, _ := os.Open(path)
defer f.Close()
stat, _ := f.Stat()
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
w.Header().Set("Accept-Ranges", "bytes")
io.Copy(w, f)
真正支持 Range 需手动解析 r.Header.Get("Range"),计算 offset/length,用 f.ReadAt —— 这部分逻辑容易出错,建议用现成库如 gofrs/flock 或直接上 nginx 做静态文件代理。
最常被忽略的一点:没校验 filepath.Clean() 后的路径是否仍在允许范围内,导致目录穿越漏洞;其次就是对 Range 请求不做处理,结果视频只能从头播。










