必须用 path.getfullpath 归一化后再校验,因为 path.combine 不归一化、字符串过滤易被 url 编码/unicode/混合斜杠/绝对路径绕过;正确流程是:归一化→加分隔符前缀检查→i/o 前执行;写操作需额外校验存在性与白名单;符号链接须单独检测。

必须用 Path.GetFullPath 归一化后再校验,仅字符串过滤 ".." 或正则替换完全不可靠。
为什么 Path.Combine + 字符串检查会失效
直接拼接用户输入和根目录后检查是否含 "..",或用 string.Replace("..", ""),根本拦不住真实攻击路径:
-
Path.Combine("C:\app", "..\..\windows\system32\cmd.exe")结果仍是C:pp....windowssystem32cmd.exe——Combine不做归一化,StartsWith判定必然通过 - URL 编码绕过:
%2e%2e/、Unicode 点号(..)、混合斜杠(..和../混用)都会逃逸字符串检测 - 绝对路径注入:
Path.Combine("C:\app", "C:\evil\web.config")直接跳转到任意驱动器,字符串检查毫无意义
正确校验流程:三步缺一不可
归一化 → 比对前缀 → 注意分隔符边界
- 先调用
Path.GetFullPath(candidate),得到操作系统最终解析的规范绝对路径(如C:pp..config.json→C:config.json) - 用
StartsWith(safeRoot + Path.DirectorySeparatorChar, StringComparison.Ordinal)检查,必须加结尾分隔符,否则C:safe会错误匹配C:safeguard - 校验必须在所有路径操作之后、文件 I/O 之前执行——比如
File.Open前、Directory.EnumerateFiles前
写文件比读文件更需严格校验
读越界最多泄露配置;写越界可能覆盖 web.config、植入 WebShell、篡改日志。
- 写操作额外检查目标路径是否已存在且不在白名单内(如禁止覆盖
appsettings.json) - 禁用扩展名黑名单(如拦截
.aspx)——Windows 允许长文件名,shell.aspx:zone.id可绕过 - 若业务允许用户指定文件名,优先用间接映射(如 ID → 文件名字典),而非拼接原始输入
符号链接是隐藏雷区
DirectoryInfo 和 File.Exists 默认跟随 symlink/junction,Path.GetFullPath 校验无效。
- 必须用
File.GetAttributes(path) & FileAttributes.ReparsePoint检测重解析点 - 再调用
File.GetLinkTarget(path)(.NET 6+)确认是否为 symlink(junction 返回 null) - 递归遍历时不能用
SearchOption.AllDirectories,必须手动 BFS/DFS 并逐项校验每个FileSystemInfo
最易被忽略的一点:校验逻辑必须跑在真实文件系统路径上,而不是 URL 解码后、未归一化的字符串。攻击者不和你比正则多厉害,只和 GetFullPath 的行为对齐——那就用它当唯一权威。










