文件预览安全沙箱:Office 文档含宏病毒?LibreOffice 隔离进程转换,防服务器感染!
去年一个做在线文档平台的哥们半夜被运维叫起来。服务器 CPU 打到 100%,磁盘疯狂读写,查了半天发现有人在后台跑挖矿脚本。溯源之后找到入口——一个用户上传的 .docm 文件,里面有段恶意 VBA 宏。系统调用 Office 转 PDF 的时候,宏被执行了,服务端直接中招。
这事儿比 SQL 注入还吓人。注入你得找到注入点,宏病毒你只要把文件上传上去,别人帮你打开,你就进去了。
文件预览几乎是所有企业应用的标配——合同管理要预览 PDF、OA 系统要预览 Word、网盘要预览 Excel。但很少有人意识到,每次把用户上传的 Office 文件扔进转换引擎,都等于在服务器上双击了一个陌生人发给你的附件。
今天聊聊怎么用进程隔离的思路,把这个风险降到最低。
宏病毒是怎么在服务端生效的
很多人觉得"我服务端又没有 Office 软件,哪里来的宏?"
但实际上,现在主流的文件预览方案底层都绕不开文档转换引擎。LibreOffice、OnlyOffice、Apache POI——它们做的事情就是把 docx / xlsx / pptx 转成 PDF 或者 HTML,然后前端渲染。而这些引擎在解析 Office 文件的时候,是可以执行宏的。
问题就在这:用户上传的文件内容是攻击者决定的。 他可以往 .docm 里塞一段 VBA 或者 Python 宏,你服务端的转换引擎一解析,宏跑了,服务器就成了肉鸡。
更隐蔽的是,攻击者不一定要拿服务器权限。他可以用宏读取服务器上的敏感文件,然后把内容通过 HTTP 请求发出去。这种数据外泄不会有 CPU 飙升,不会有磁盘异常,静悄悄就把你的数据偷走了。
所以核心问题不是"LibreOffice 有没有漏洞",而是你不能信任任何用户上传的文件内容。这份不信任必须贯穿整个处理链路。
思路:让危险的东西跑在笼子里
处理思路很直白——把转换进程关进隔离环境,跟业务服务器物理隔开。
用户上传 .docm
│
├─ 保存到临时目录
│
├─ 启动隔离容器 / 子进程
│ ├─ 只挂载临时目录(能读能写)
│ ├─ 无网络权限(宏发不出数据)
│ ├─ CPU / 内存限额(防挖矿占满资源)
│ └─ 超时自动强杀(防死循环)
│
├─ 容器内执行 LibreOffice 转 PDF
│
├─ 读取转换后的 PDF
│
└─ 销毁容器 + 清理临时文件
转换进程跑在一个与世隔绝的环境里。即使宏被执行了,它能访问的文件只有临时目录里的那几份,能消耗的 CPU 有上限,想往外发数据——没有网络。
这就是"沙箱"的思路:不阻止宏执行,而是让宏执行了也什么都做不了。
方案落地:Docker 容器沙箱 vs 子进程沙箱
实现隔离有两种主流方式:
方式一:Docker 容器沙箱(隔离性最强)
每次转换任务启动一个新的 Docker 容器,里面装 LibreOffice。转换完成后销毁容器。
转换流程:
docker run --rm \
--network none \ # 禁用网络
--memory 512m \ # 限制内存
--cpus 1 \ # 限制 CPU
--read-only \ # 根文件系统只读
-v /tmp/input:/input:ro \ # 只读挂载输入
-v /tmp/output:/output:rw \ # 读写挂载输出
--timeout 30 \ # 超时秒数
libreoffice-image \
soffice --headless --convert-to pdf /input/file.docx --outdir /output
关键参数就是那几行:--network none 断网,--memory 512m 限内存,--cpus 1 限 CPU,--read-only 根文件系统只读。这些组合在一起,宏执行了也没用——读不到敏感文件、占不满 CPU、发不出数据。
隔离性满分,但代价也很明显:Docker 启动有冷启动开销,一个 docker run 大概要 0.5 到 2 秒。频繁创建销毁容器对系统也有压力。
方式二:操作系统子进程隔离(性能好,隔离性够用)
如果不想背 Docker 的启动开销,可以在宿主机上直接用进程级别的隔离。Linux 上有现成的工具:
# 用 unshare 创建隔离的命名空间
unshare --net --pid --fork --mount-proc \
timeout 30 \
soffice --headless --convert-to pdf /input/file.docx --outdir /output
unshare --net 让子进程跑在独立的网络命名空间里,它看不到宿主机的网卡,只能看到一个 lo 回环接口——发什么都不可能出去。timeout 30 做超时保护,超时直接 SIGKILL。
这种方式启动几乎是瞬间的(毫秒级),资源开销也小,适合高并发场景。缺点是隔离性不如 Docker 完整——进程仍然共享宿主机的文件系统视图,虽然有权限控制,但没有 Docker 那种"根文件系统只读"的硬隔离。
怎么选?
高安全要求(金融、政务)→ Docker 沙箱
高并发要求(日均 10 万次转换)→ 进程隔离 + 额外的安全加固
两者都有 → 容器预热池,常驻几个容器,任务来了直接复用
完整的转换沙箱流程
把上面的内容串成一个完整的服务:
用户上传 .docx
│
├─ Step 1: 文件类型校验
│ ├─ 检查文件 Magic Number(不是后缀名)
│ └─ 不在白名单内 → 拒绝
│
├─ Step 2: 写入隔离目录
│ ├─ 随机生成 taskId
│ └─ 复制到 /tmp/sandbox/{taskId}/input/
│
├─ Step 3: 启动沙箱转换
│ ├─ 网络隔离 + CPU/内存限制 + 30 秒超时
│ ├─ 执行 soffice --headless --convert-to pdf
│ └─ 输出到 /tmp/sandbox/{taskId}/output/
│
├─ Step 4: 读取结果
│ ├─ 检查输出 PDF 是否正常生成
│ ├─ 验证 PDF 文件头(防伪造)
│ └─ 返回 PDF 给前端预览
│
└─ Step 5: 清理
└─ 删除 /tmp/sandbox/{taskId}/ 整个目录
这里面有几处细节值得单拎出来说。
文件类型校验不能看后缀名。 攻击者把 .exe 改名成 .docx 上传,你如果只看后缀名就直接扔给 LibreOffice,运气好的话转换失败报个错,运气不好呢?用文件头(Magic Number)校验最靠谱,比如 docx 的前几个字节是 50 4B 03 04(PKZip)。
Step 3 的超时一定要设。 有些恶意文档会故意构造得极其复杂,让 LibreOffice 解析的时候陷入死循环或者无限膨胀。设个 30 秒的超时,到期就强杀,别让它一直耗着。
Step 5 的清理要在 finally 块里做。 异常也要清理,不能因为转换失败就留一地临时文件。
几个头疼的极端场景
LibreOffice 挂了怎么办
LibreOffice 不是特别稳,某些复杂文档可能会让它崩溃。处理方式:捕获子进程的退出码,如果非 0,记日志,返回"文件无法预览"。不要尝试重试——如果是文件本身触发了崩溃,重试多少次都会崩,反而浪费资源。
并发转换把服务器打满
假设同时来了 50 个转换请求,每个 LibreOffice 进程吃 200MB 内存,光转换进程就占 10GB。如果没有并发控制,内存直接爆。
用一个信号量或者线程池限制并发数:
MAX_CONCURRENT = 10
信号量 = new Semaphore(MAX_CONCURRENT)
转换函数:
信号量.acquire()
try:
执行转换
finally:
信号量.release()
超过上限的请求排队等待。宁可让用户多等 3 秒,也别让服务器 OOM。
PDF 没生成但进程没报错
超时杀或者 LibreOffice 静默失败的时候,输出目录里可能没有 PDF 文件。Step 4 读取结果前务必检查文件是否存在。有一个额外的细节:可以在转换完成后校验一下 PDF 文件头是否为 %PDF-,防止生成了损坏的文件被当做正常结果返回。
总结
文件预览安全这件事,一句话就是:不要在你的主进程里打开任何用户上传的文件。
方案上,Docker 容器沙箱隔离性最强,适合对安全要求高的场景。操作系统子进程隔离性能更好,适合高并发场景。但不管是哪种,核心要素都一样:
网络隔离 — 宏发不出数据。用 --network none 或者 unshare --net。
资源限制 — 挖矿占不满 CPU。用 cgroup 或者 Docker 的 --memory、--cpus。
超时强杀 — 死循环耗不死你。timeout 30,到点就结束。
用完即弃 — 每次转换独立环境,不残留状态。临时文件和进程全部清理。
这些做到位,用户上传的 .docm 里不管藏了什么,最坏的结果就是转换失败——而不是你的服务器变成矿机。
有用的话转给你们组里还在主进程跑 soffice 的后端。
标题:文件预览安全沙箱:Office 文档含宏病毒?LibreOffice 隔离进程转换,防服务器感染!
作者:jiangyi
地址:http://jiangyi.space/articles/2026/06/04/1780411962872.html
公众号:服务端技术精选
评论