← 中文资源

简体中文版本

Tar 解压路径防护

解压前校验压缩包条目名称,阻止绝对路径、上级目录穿越以及任何逃出目标目录的路径。

阅读英文原文

功能说明

压缩包解压是路径穿越的高发场景:条目名可以写成绝对路径或以多个上级目录开头,天真的实现会把文件写到目标目录之外,甚至覆盖系统文件。这个防护函数在真正创建文件之前检查每个条目,只允许层层向下的相对路径。

main 用五组典型条目演示:正常文件、包内回退、越界回退、绝对路径和连续越界。允许包内回退是因为 docs/../notes.txt 最终仍落在目标目录内,属于合法整理;只要任何一步会爬到目标之外就拒绝。

关键代码

Path::components 把条目名拆成普通段、当前目录段、上级段和根前缀。深度计数与 output.pop 同步,遇到上级段时先确认深度大于零,再弹出一段。绝对路径与平台前缀直接拒绝。最后用 starts_with 按路径组件比较,确保拼接结果仍在目标目录中,避免相似前缀绕过。

使用注意

符号链接条目是另一类风险:即使路径合法,链接也可能指向外部,解压后写入会跟随链接。实际使用 tar 库时应禁止或单独校验链接目标,并优先使用不跟随链接的创建标志。该函数不处理 Windows 设备名和以点结尾的文件名。目标目录本身应使用规范化后的绝对路径。检查与写入之间仍有竞态,高安全场景应直接在解压库中启用路径校验。

验证方式

reports/2026.csv 与 docs/../notes.txt 被允许,输出最终路径;以两个点开头、绝对路径以及 a/../../b 都被拒绝并给出原因。可以加入空名称与只含当前目录的条目,确认前者被拒绝、后者指向目标目录本身。再构造一个符号链接条目,提醒需要额外的链接检查。

代码示例

use std::path::{Component, Path, PathBuf};

fn safe_entry_path(destination: &Path, entry_name: &str) -> Result<PathBuf, String> {
    let entry = Path::new(entry_name);
    if entry.is_absolute() {
        return Err(format!("absolute entry rejected: {entry_name}"));
    }

    let mut output = destination.to_path_buf();
    let mut depth = 0usize;
    for component in entry.components() {
        match component {
            Component::Normal(part) => {
                output.push(part);
                depth += 1;
            }
            Component::CurDir => {}
            Component::ParentDir => {
                if depth == 0 {
                    return Err(format!("entry escapes destination: {entry_name}"));
                }
                output.pop();
                depth -= 1;
            }
            _ => return Err(format!("unsupported component in {entry_name}")),
        }
    }

    if !output.starts_with(destination) {
        return Err(format!("resolved entry escapes destination: {entry_name}"));
    }
    Ok(output)
}

fn main() {
    let destination = Path::new("/var/lib/app/uploads");
    let entries = [
        "reports/2026.csv",
        "docs/../notes.txt",
        "../etc/cron.d/job",
        "/etc/passwd",
        "a/../../b",
    ];
    for entry in entries {
        match safe_entry_path(destination, entry) {
            Ok(path) => println!("ok   {entry:<22} => {}", path.display()),
            Err(error) => println!("skip {entry:<22} => {error}"),
        }
    }
}