必须用go-clamd直连clamd守护进程做流式扫描,因clamscan命令行每次fork开销大、临时文件存在竞态与权限风险(如noexec挂载、硬链接攻击),且文件落地后到扫描前有篡改窗口;STREAM模式无需落盘,需配StreamSaveToDisk no,通过client.StreamScan(reader)传multipart.Reader实现安全高效扫描。

必须用 go-clamd 直连 clamd 守护进程做流式扫描,不能调 clamscan 命令行或等文件落地再扫——否则竞态、权限、超时全会出问题。
为什么不能用 exec.Command("clamscan", path)
每次调 clamscan 都要 fork 新进程,启动开销大;临时文件路径若被攻击者提前创建硬链接或符号链接,扫描时可能误判或漏判;/tmp 目录常挂载 noexec 或 nosuid,导致子进程直接失败。更关键的是:文件写完到执行命令之间存在时间窗口,恶意内容可能已被篡改或触发其他逻辑。
- clamd 守护进程复用连接池,响应快、权限可控(可指定运行用户为
clamav) - 流式扫描(
STREAM命令)无需落盘,避免磁盘 I/O 和路径遍历风险 -
go-clamd封装了协议细节,自动处理换行、空格、特殊字符,比手拼"SCAN /path\n"可靠得多
流式扫描必须配对设置 StreamSaveToDisk no
clamd.conf 中的 StreamSaveToDisk 默认是 yes,但流式扫描(STREAM)场景下必须显式设为 no。设成 yes 会导致 clamd 把流内容强制写入磁盘再扫,既失去流式优势,又引入和落地扫描一样的竞态与权限问题。
- 检查配置:运行
sudo grep StreamSaveToDisk /etc/clamav/clamd.conf,确认输出为StreamSaveToDisk no - 重启服务:
sudo systemctl restart clamav-daemon(Debian/Ubuntu)或sudo systemctl restart clamd@scan(RHEL/CentOS) - Go 客户端初始化时传对地址:
clamd.New("unix:///var/run/clamd.scan/clamd.sock")或clamd.New("tcp://127.0.0.1:3310")
SCAN vs STREAM:选哪个?
二者协议不同、适用场景不同。SCAN 要求文件已存在于 clamd 可读路径,适合小批量、已落盘的文件;STREAM 接收原始字节流,适合上传直通扫描,无中间文件。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 用
client.ScanFile(path)→ 对应SCAN命令,path 必须是 clamd 进程有读权限的绝对路径(如/tmp/upload_abc.bin),且需提前os.Chmod(path, 0444) - 用
client.StreamScan(reader)→ 对应STREAM命令,直接传*multipart.FileHeader.Open()返回的io.Reader,不生成临时文件 - 注意:调
StreamScan前必须确保clamd.conf中StreamSaveToDisk no,否则返回ERROR: Stream: Invalid argument
扫描结果处理不能只看 FOUND 就删文件
返回 "stream: Win.Trojan.XYZ FOUND" 只表示检测命中,不代表处置完成。忽略错误、跳过日志、直接返回 200,等于给攻击者留后门。
- 必须检查
error:网络断开、clamd 未响应、协议解析失败都会返回非 nil error,此时应返回http.StatusServiceUnavailable并记录告警,而非放行 - FOUND 时禁止返回具体病毒名,HTTP 状态码用
http.StatusBadRequest或http.StatusForbidden,响应体只写泛化提示如"File failed security validation" - 扫描前生成唯一
upload_id(如 UUID),所有日志、临时路径、数据库记录都带该 ID,便于溯源和审计 - 不要只
os.Remove(tmpPath)—— 若使用 STREAM 模式则无临时文件;若用 SCAN 模式,删完还要os.RemoveAll(filepath.Dir(tmpPath))清空整个临时目录层级,防残留
最易被忽略的是三段超时独立控制:HTTP 层(ReadTimeout)、clamd 连接层(net.Dialer.Timeout)、clamd 扫描层(context.WithTimeout)。任一段没设限,都可能让 goroutine 卡死,拖垮整个服务。

















