← 中文资源

简体中文版本

路径规范化防越界

逐段规范化不可信的相对路径,拒绝绝对路径与任何试图爬出根目录的上级引用。

阅读英文原文

功能说明

用户上传、压缩包解压和模板渲染常把外部提供的相对路径拼到根目录下。直接拼接字符串会让 ../../etc/passwd 这样的输入逃出根目录。这个函数逐段处理路径:普通段追加到结果,当前目录段忽略,上级段必须在已有深度大于零时才能弹出一段,绝对路径和平台前缀直接拒绝。

处理完成后还会拼到根目录并再次检查前缀,形成双保险。main 用五组输入演示合法路径、内部回退、越界回退、绝对路径和连续越界。

关键代码

Path::components 会按平台规则拆分路径,Windows 的反斜杠也能识别。深度计数与 PathBuf::pop 同步,确保每弹出一段都有对应的已压入段;一旦深度为零又遇到上级段立即失败。root.join 之后用 starts_with 比较路径组件而不是字符串前缀,避免相似目录名绕过。错误信息包含原始输入,便于审计。

使用注意

该函数只做词法规范化,不解析符号链接;根目录内若有指向外部的链接,仍需在打开文件时使用 nofollow 或解析真实路径。Windows 保留设备名和结尾点号问题需另行处理。空路径会返回根目录本身,业务上应拒绝。路径长度限制由文件系统决定。安全检查与真正打开文件之间存在竞态,高安全场景应基于文件描述符操作。

验证方式

reports/2026/q1.csv 正常拼接;reports/../notes.txt 规范化后仍位于根目录内,允许通过;以两个点开头的路径、绝对路径 /etc/passwd 以及 a/../../b 都被拒绝,输出 skip 与原因。可以加入双反斜杠、空段和只含当前目录的输入,确认结果仍不越界。

代码示例

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

fn normalize_join(root: &Path, untrusted: &str) -> Result<PathBuf, String> {
    let candidate = Path::new(untrusted);
    if candidate.is_absolute() {
        return Err(format!("rejected absolute path: {untrusted}"));
    }

    let mut depth = 0usize;
    let mut normalized = PathBuf::new();
    for component in candidate.components() {
        match component {
            Component::Normal(part) => {
                normalized.push(part);
                depth += 1;
            }
            Component::CurDir => {}
            Component::ParentDir => {
                if depth == 0 {
                    return Err(format!("path escapes root: {untrusted}"));
                }
                normalized.pop();
                depth -= 1;
            }
            _ => return Err(format!("unsupported path component in {untrusted}")),
        }
    }

    let joined = root.join(&normalized);
    if !joined.starts_with(root) {
        return Err(format!("resolved path escapes root: {untrusted}"));
    }
    Ok(joined)
}

fn main() {
    let root = Path::new("/srv/uploads");
    let candidates = [
        "reports/2026/q1.csv",
        "reports/../notes.txt",
        "../secrets.env",
        "/etc/passwd",
        "a/../../b",
    ];
    for candidate in candidates {
        match normalize_join(root, candidate) {
            Ok(path) => println!("ok   {candidate:<22} => {}", path.display()),
            Err(error) => println!("skip {candidate:<22} => {error}"),
        }
    }
}