std::filesystem::lexically_normal() 不能防止路径遍历攻击,它仅做词法归一化、不访问文件系统、不验证存在性或权限,无法阻止如 ../../../etc/passwd 的恶意路径。

用 std::filesystem::path::lexically_normal() 不能防路径遍历
直接说结论:std::filesystem::lexically_normal() 只做词法归一化,不验证路径是否存在、也不检查权限,完全无法阻止 ../ 攻击。它只是把 a/../b 变成 b,但前提是输入是合法路径字符串——而恶意用户传 ../../../etc/passwd,它照样给你归一成 /etc/passwd,毫无防护作用。
常见错误现象:代码里写了 p.lexically_normal() 就以为安全了,结果 Web 服务或配置加载器仍能被读取任意文件。
-
lexically_normal()不访问文件系统,纯字符串处理 - 它不区分相对路径和绝对路径,
../../secret.txt归一后仍是相对路径(除非你提前 resolve) - 它不校验路径是否在允许根目录之下——这才是防护核心
真正有效的做法:白名单根目录 + std::filesystem::canonical() 或 std::filesystem::weakly_canonical()
必须先限定允许访问的根目录(如 /var/www/uploads),再将用户输入路径与之拼接,最后用 canonical() 解析真实路径并检查是否仍在根内。
关键点在于:canonical() 会实际访问文件系统,解析符号链接、处理 ..,返回绝对且唯一的路径;如果目标不存在,会抛 std::filesystem::filesystem_error,这反而是好事——可直接拒绝。
- 用
std::filesystem::path base_dir = "/var/www/uploads";明确声明可信根 - 拼接后立即调用
std::filesystem::canonical(user_path)(不是lexically_normal) - 检查结果路径是否以
base_dir开头:real_path.string().starts_with(base_dir.string()) - 若用户路径指向不存在的位置,
canonical()抛异常,捕获后拒绝即可
示例片段:
std::filesystem::path base_dir{"/var/www/uploads"};
std::filesystem::path user_input{"../../etc/shadow"}; // 恶意输入
auto full_path = base_dir / user_input;
try {
auto real_path = std::filesystem::canonical(full_path);
if (real_path.string().starts_with(base_dir.string())) {
// 安全,可打开
} else {
throw std::runtime_error("Path escape detected");
}
} catch (const std::filesystem::filesystem_error&) {
throw std::runtime_error("Invalid or inaccessible path");
}
注意 weakly_canonical() 的适用场景和风险
当你要处理的路径可能最终不存在(比如创建新文件前校验父目录),canonical() 会失败,这时可用 weakly_canonical() ——它只解析存在的部分,对末尾不存在的组件保留原样。
但它有坑:如果恶意路径中存在一个**真实存在的中间目录**(如 ../../tmp/existing_dir/../../../etc),weakly_canonical() 可能只归一到 /tmp/existing_dir/../../../etc,后面仍含 ..,导致后续判断失效。
- 仅在明确需要支持“路径末尾不存在”时才用
weakly_canonical() - 用它之后,仍需手动展开剩余的
..(比如再套一层lexically_normal()),但必须配合严格前缀检查 - 更稳妥的做法是:先
weakly_canonical(),再对结果调用lexically_normal(),最后比对是否在根下——但依然要确保根是绝对路径且无符号链接绕过
容易被忽略的底层细节:符号链接与挂载点
即使做了 canonical() + 前缀检查,如果根目录本身是符号链接(如 /var/www/uploads → /mnt/nfs/uploads),攻击者可能利用跨文件系统符号链接逃逸。Linux 下 canonical() 默认不跟随挂载点外的符号链接,但行为依赖 OS 和挂载选项。
- 生产环境应确保
base_dir是真实物理路径,用std::filesystem::canonical(base_dir)预先解析一次 - 避免在根目录上使用符号链接;若必须,考虑用
openat()+AT_SYMLINK_NOFOLLOW等系统调用级防护(C++ 标准库不覆盖此层) - 容器或沙箱环境里,还要配合
chroot或命名空间限制,canonical()无法替代这些机制
归一化函数不是防火墙,路径校验本质是「信任边界」问题——你得亲手划清那条线,并确保所有路径都落到线内。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











