ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Strata 压缩包解压安全机制全解析:Zip Slip 防御与 unsafe 代码策略完整指南

Strata 压缩包解压安全机制全解析:Zip Slip 防御与 unsafe 代码策略完整指南

【免费下载链接】strata

The best file manager on Linux

项目地址:https://gitcode.com/gh_mirrors/strata10/strata
点击查看免费下载

Strata是 Linux 上体验最好的文件管理器之一,也是 Rust 编写的 GTK 应用。本文深入介绍 Strata 压缩包解压安全机制:它如何防御经典的Zip Slip 路径穿越漏洞、如何拒绝符号链接遍历,以及项目如何通过编译期 lint 近乎彻底地禁止 unsafe Rust 代码。

先看 Strata 的列视图界面,解压动作就藏在你每天使用的右键菜单里:

什么是 Zip Slip:压缩包里藏着的"越狱"路径

🕵️ 压缩包(ZIP / 7Z / RAR / TAR)里每个文件的"名字"本质上是不可信的用户输入。攻击者可以手工构造一条名为../../etc/cron.d/backdoor的成员路径:

  • 若解压程序把"目标文件夹 + 成员路径"直接拼接,../会一路把文件写到你选择的目录之外;
  • 这就是臭名昭著的Zip Slip漏洞,曾让无数主流解压工具"中枪"。

Strata 的官方设计文档对此有明确定义:

如果解压器把选中文件夹与../report.txt原样拼接,文件系统会把结果解析到该文件夹旁边而不是里面。

完整说明见 docs/archives.md。

第一道防线:成员路径净化,../被强制钳制在解压目录内

核心函数是 sanitized_archive_path,它对每个成员路径逐段处理,规则非常克制:

输入形态处理结果
../report.txt净化为report.txt(留在目录内)
folder/../report.txt净化为report.txt
/tmp/marker、\\server\share❌ 直接拒绝(绝对路径 / UNC 路径)
C:\tmp\marker、C:marker❌ 直接拒绝(Windows 盘符前缀)
空路径❌ 直接拒绝

几个值得注意的细节:

  • 🔒单个越界成员不会毁掉整个解压:路径被"钳制"后解压继续处理后续成员,而不是整体中止;
  • ⚔️反斜杠一并拦截:safe\..\..\marker中的\会先被归一化为/,Windows 风格的穿越同样无处遁形;
  • 📛冲突自动改名:两条成员净化后都叫report.txt时,会生成report.txt和report (2).txt,绝不互相覆盖。

对应的单元测试把攻击向量写得明明白白:/tmp/marker、C:\tmp\marker、//server/share/marker全部必须被拒绝,见 destination/tests.rs#L21-L55。

第二道防线:固定目录描述符 + NOFOLLOW,文件系统层面拒绝符号链接

光净化字符串还不够——攻击者还可以在解压进行到一半时,把目录树里的某个节点偷偷换成符号链接,诱导写入被重定向。Strata 在 ExtractionDestination 里用操作系统机制堵死了这条路:

  • 📌固定根目录(pinned fd):解压目标通过openat2打开后,后续所有操作都以这个文件描述符为基准(descriptor-relative),即使路径中途被替换也无法劫持写入;
  • 🚫逐级O_NOFOLLOW下降:每一级子目录都以DIRECTORY | NOFOLLOW打开(见 descend),符号链接根本无法被"当作目录"穿越;
  • 🎭隐藏 staging 目录:文件先写入.strata-extraction-<id>暂存目录,解压全部完成后才一次性发布。密码错误时暂存内容被整体丢弃,失败/取消时残留文件则保留在归档同名文件夹中,方便排查;
  • ⚡no-replace 原子发布:最终发布使用"绝不覆盖"的目录重命名,目标已存在时自动改成photos (1)/、photos (2)/,永远不会替换你的既有文件;
  • 🔐权限位过滤:每个成员恢复存储的权限位时,setuid / setgid / sticky 位被强制剥离,再叠加 umask;FIFO 和设备节点直接拒绝创建。

解压入口本身也做了校验

🖱️ 只有"常规文件(或指向常规文件的符号链接)+ 本地路径 + 已识别的压缩包扩展名"才会提供Extract here/Extract to…菜单项。命名为photos.zip的文件夹、FIFO、socket、断链统统不算数。即使你打开菜单后文件被外部修改,操作层还会在执行前二次校验路径——这就是图中右键菜单的行为来源:

另外,只有解码器明确报告"密码缺失或错误"才会弹出密码框;错误文案永远不参与判断,所以压缩包成员名里包含 "password" 字样也骗不出密码输入框。

unsafe 代码策略:Rust 的"默认拒绝"如何落地

🛡️ 解压安全还建立在底层语言之上:Strata 是一个safe-Rust 代码库,unsafe是例外而非常规工具。完整策略写在 docs/unsafe-code.md,而强制手段来自 Cargo.toml 的 lint 配置:

Lint 规则效果
unsafe_code = "deny"全局禁止 unsafe 代码
unsafe_op_in_unsafe_fn = "deny"禁止在 unsafe 函数里藏 unsafe 操作
multiple_unsafe_ops_per_block = "deny"每个 unsafe 块只允许一个unsafe 操作
undocumented_unsafe_blocks = "deny"每个 unsafe 块必须写SAFETY:注释
missing_safety_doc = "deny"公开 unsafe 函数必须有安全文档
禁止#[allow(...)]只能用带 reason 的#[expect(...)],删掉操作后编译器会立刻报"过期的例外"

CI 用 Clippy 运行且warning 即 error,任何违规都会直接阻断合并。

目前整个仓库只有两处经过批准的例外:

  1. 📄字体注册(src/assets.rs):fontconfig 的 C API 没有合适的 safe 封装,仅在单线程启动期使用 3 个 FFI 调用;
  2. 🧱浏览器沙箱 fork 边界(src/sandbox/browser/):fork+ namespace 隔离没有 safe API,用于把媒体解码器关进一次性进程沙箱——这本身也是预览安全的一部分。

小结:三层纵深防御如何协同

✅ Strata 的压缩包解压安全不靠单点,而是层层设防:

  1. 路径净化层——成员路径逐段解析,..钳制在解压根内,绝对路径与盘符前缀直接拒绝;
  2. 文件系统层——固定描述符 + NOFOLLOW 下降 + staging 目录 + no-replace 发布 + 权限位剥离,符号链接与中途替换攻击全部失效;
  3. 语言与流程层——safe-Rust + 编译期 lint 封死 unsafe 后门,解压目标二次校验、密码弹窗只信解码器结果。

对普通用户而言,这意味着:无论打开来路多么可疑的压缩包,解压产物都只可能落在你选的文件夹里——这正是"最好的文件管理器"应该给出的安全感。

【免费下载链接】strata

The best file manager on Linux

项目地址:https://gitcode.com/gh_mirrors/strata10/strata
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进