必须校验文件后缀、mime类型及内容:用python-magic检测前1024字节,仅允白名单mime;clamav须用instream内存扫描并处理连接异常,禁跳过小文件扫描。

上传前必须校验文件后缀和 MIME 类型
光靠 filename 后缀判断类型极不可靠,攻击者随便改个 .jpg 后缀就能传入恶意可执行文件。浏览器发来的 Content-Type 也完全不可信,能被任意篡改。
真正有效的做法是读取文件前几百字节,用 python-magic 做实际内容检测:
import magic<br>mime = magic.from_buffer(file_bytes, mime=True)
只允许白名单 MIME 类型,比如 image/jpeg、application/pdf,拒绝 application/x-executable 或 text/html(HTML 文件可能含 xss 或下载诱导脚本)。
- 别用
file.content_type—— 它只是 http 请求头里的字符串,毫无约束力 -
magic.from_file()不能用于临时上传流,必须用from_buffer()配合read(1024) - 某些 PDF 病毒会伪装成
application/pdf,但真实扫描需更深层分析,这一步只是基础过滤
调用 ClamAV 扫描时别直接传文件路径
用户上传的文件若直接落盘再传给 clamdscan 或 clamscan,存在竞态条件:文件刚写完、还没扫完,就被另一个进程或脚本读取执行了。更糟的是,如果路径可控,还可能触发路径遍历(比如传 ../../../.ssh/id_rsa)。
立即学习“Python免费学习笔记(深入)”;
正确姿势是走内存或 socket 通信,避免落地:
import clamd<br>cd = clamd.ClamdAgnostic()<br>result = cd.instream(file_bytes)
-
instream()把二进制内容直接发给clamd守护进程,不写磁盘,无路径风险 - 确保
clamd配置里StreamMaxLength足够大(默认 25M),否则大文件会返回INSTREAM Error: Stream: File size limit exceeded - 如果用
clamscan命令行,必须配合tempfile.NamedTemporaryFile(delete=False)+ 扫描后立即os.unlink(),但依然不如instream干净
ClamAV 返回结果要区分 “未发现” 和 “扫描失败”
很多人只检查 result['stream'] == 'OK' 就放行,但 ClamAV 的响应结构其实有三种关键状态:
-
'stream': 'OK'→ 无病毒(安全) -
'stream': 'FOUND'→ 发现病毒,result['stream']值是病毒名,如Win.Trojan.Keylogger-12345 - 根本没返回
'stream'键,或抛出clamd.ConnectionError/socket.timeout→ 扫描异常,不能当“干净”处理
漏掉第三种情况等于留后门:ClamAV 服务挂了、超时了、配置错了,上传就自动通过。生产环境必须显式拦截这类异常,并记录日志,例如:
try:<br> result = cd.instream(file_bytes)<br>except (clamd.ConnectionError, socket.timeout):<br> raise RuntimeError("Virus scan unavailable")
小文件跳过扫描?别省这点 CPU
有人觉得 1KB 的文本文件不可能带病毒,就加逻辑跳过扫描。错。很多 Office 文档宏病毒、LNK 文件、恶意 PDF 元数据都只有几 KB,且 ClamAV 对小文件的 instream 调用耗时通常在毫秒级。
真正该优化的是并发和超时,不是按大小跳过:
- ClamAV 默认单线程处理
instream,高并发下会排队,建议启用clamd的MaxThreads配置 - 给
instream()加timeout=30参数,避免卡死(ClamAV 处理畸形 PDF 有时会 hang 住) - 如果真要限流,用请求队列或令牌桶,而不是基于文件大小做信任降级
病毒不挑体积,只挑疏忽。只要走上传流程,就得走全链路校验 —— 后缀、MIME、内容扫描,缺一不可。