乐于分享
好东西不私藏

文件预览安全沙箱:Office 文档含宏病毒?LibreOffice 隔离进程转换,防服务器感染!

文件预览安全沙箱: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 的后端。