std::filesystem::path::lexically_normal仅做字符串规范化,无法防止路径遍历;真正安全需结合weakly_canonical()解析真实路径、is_sub_directory_of()校验父子关系,并锁定根路径或驱动器。

std::filesystem::path::lexically_normal 不能防止路径遍历
lexically_normal() 只做字符串规范化:合并 ./、展开 ../、去重斜杠。它不检查路径是否越界,也不感知文件系统实际结构。比如 "../../../etc/passwd" 经 lexically_normal() 后变成 "/etc/passwd" —— 看似“干净”了,但危险一点没少。
常见错误现象:lexically_normal() 后直接拼接根目录(如 base / user_path.lexically_normal()),以为这就安全了;结果攻击者提前在输入里埋好足够多的 ../,绕过你自以为的“规范化”边界。
- 它不校验路径是否落在授权目录内,只做字面归一化
- 对符号链接完全无感,
lexically_normal()不解析链接目标 - 在 Windows 上处理
\?C:或 UNC 路径时行为不一致,容易漏判
真正有效的白名单校验必须用 std::filesystem::weakly_canonical + is_sub_directory_of
先用 weakly_canonical() 解析真实路径(会跟随符号链接、消除所有相对跳转),再用 is_sub_directory_of() 判断是否严格位于允许目录之下。这才是防遍历的正确链路。
使用场景:Web 服务读取用户上传的文件名、配置中指定模板路径、插件加载时解析资源路径。
-
weakly_canonical()会抛异常(如权限不足、路径不存在),需捕获std::filesystem::filesystem_error - 基目录必须是绝对路径,且最好提前调用
weakly_canonical()固化一次,避免每次比较都重复解析 - Windows 下注意驱动器盘符大小写和 UNC 路径前缀,建议统一转小写并标准化为
\servershare形式再比对
示例关键逻辑:
std::filesystem::path base = std::filesystem::weakly_canonical("/var/www/static");
std::filesystem::path user_input = "img/../img/../../etc/passwd";
try {
auto resolved = std::filesystem::weakly_canonical(base / user_input);
if (resolved.is_sub_directory_of(base) || resolved == base) {
// 安全,可读
} else {
// 拒绝:已跳出基目录
}
} catch (const std::filesystem::filesystem_error&) {
// 路径无效或不可访问
}
构造路径时别用 operator/ 拼接未清洗的字符串
直接 base / user_str 是高危操作:如果 user_str 是 "../.ssh/id_rsa",拼出来就是 /var/www/../.ssh/id_rsa,后续哪怕再 lexically_normal() 也晚了——因为 operator/ 已经把恶意结构注入进 path 对象内部。
- 永远先将用户输入构造成
std::filesystem::path,再做净化与校验 - 禁止用
std::string::find("..")这类字符串扫描代替路径语义校验——绕过方式太多(%2e%2e、..、Unicode 点号等) - 若必须从 URL 或表单解析路径,优先用框架提供的解码+路径分离工具(如
std::url的草案实现),而非手撕substr
Windows 下额外要拦住驱动器切换和全局路径
Linux 用户常忽略这点:Windows 允许 C:oo 和 D:ar 并存,而 is_sub_directory_of("C:/a") 对 "D:/b" 返回 false —— 这没错,但如果你的基目录是 "C:/app/data",攻击者传 "C:/windows/system32/drivers/etc/hosts" 就能绕过校验,因为两者同属 C 盘但不在子目录关系中。
- 必须显式检查
resolved.root_name() == base.root_name()(Windows)或resolved.root_path() == base.root_path()(跨平台更稳) - 禁用
\?、\.、UNC路径前缀,它们会绕过常规路径限制 - 对
GetFullPathNameA/W返回的路径做二次weakly_canonical(),因为 API 本身不解析符号链接
lexically_normal 就只是个好看的装饰。C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











