zipfile.extracttodirectory 存在路径遍历安全风险,因不校验条目名(如../../../etc/shadow),可导致文件写入任意目录;必须改用 ziparchive 手动校验每个 entry.fullname 是否越界或含控制字符。

用 ZipFile.ExtractToDirectory 解压 ZIP 文件最省事,但默认不校验路径,生产环境直接用等于裸奔。
为什么 ZipFile.ExtractToDirectory 会出安全问题
它内部不做任何路径合法性检查。如果 ZIP 包里有个条目叫 ../../../etc/shadow,调用后就会被解压到系统关键位置——这就是路径遍历攻击。
常见错误现象:UnauthorizedAccessException 或文件莫名出现在不该出现的目录;更糟的是静默覆盖,连异常都不抛。
- 不是所有 ZIP 都来自可信源,CI 构建、用户上传、第三方 API 返回都可能带恶意包
-
ZipFile.ExtractToDirectory的重载里没有内置校验开关,必须自己加 - 即使目标路径用了
Path.GetFullPath,也拦不住..\在 ZIP 条目名里的穿透
怎么安全地解压 ZIP(推荐手动校验)
绕开 ZipFile.ExtractToDirectory,改用 ZipArchive 遍历每个 ZipArchiveEntry,逐个判断再提取。
核心校验逻辑就两条:
- 用
Path.GetRelativePath(.NET Core 2.1+)或正则^\.\./检查entry.FullName是否含上级跳转 - 确认
entry.FullName不以/、\开头,且不含控制字符(如\0、\x01)
示例片段:
ApiPost是一个支持团队协作,支持模拟POST、GET、PUT等常见请求,并可直接生成文档的API调试、管理工具,ApiPost是后台接口开发者或前端、接口测试人员的工作必备工具。快速生成、一键导出API文档。感兴趣的朋友快来下载吧。软件说明ApiPost官方版是一款十分出色的接口调试与文档生成工具,ApiPost官方版界面美观大方,功能强劲实用,支持团队协作,支持模拟POST、GET、PUT等常见请求,是后台接口开发者或前端、接口测试人员的工作必备工具。软件特色更方便支持接口调试的同时快速生成、一键
using (var archive = ZipFile.OpenRead(zipPath))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
string safePath = Path.Combine(extractPath, entry.FullName);
// 校验:不能跳出 extractPath
if (Path.GetRelativePath(extractPath, safePath).StartsWith("..") ||
Path.IsPathRooted(entry.FullName) ||
entry.FullName.Contains('\0'))
{
throw new InvalidOperationException($"Unsafe entry: {entry.FullName}");
}
entry.ExtractToFile(safePath, overwrite: true);
}
}
压缩单个文件时别硬套 ZipFile.CreateFromDirectory
这个方法只接受目录路径,强行传入单个文件会报 DirectoryNotFoundException。
正确做法是用 ZipArchive 手动建包:
- 新建
FileStream(FileMode.Create),再套ZipArchive(ZipArchiveMode.Create) - 调用
CreateEntry命名条目,再用Open()获取流写入内容 - 务必等所有
entry.Open()写完、流关闭后再 dispose archive 和 fileStream
漏掉最后一步,ZIP 结构就损坏,WinRAR 提示 InvalidDataException: End of Central Directory record could not be found。
时间戳和符号链接这两个坑容易被忽略
ZipFile.CreateFromDirectory 会把所有文件的时间戳统一设为打包时刻,丢失原始 LastWriteTime;Linux/macOS 下的符号链接会被直接跳过,不报错也不存档。
如果你在做 CI 构建、跨平台部署或需要审计溯源:
- 必须放弃
CreateFromDirectory,改用ZipArchive+ 手动设置entry.LastWriteTime - 对 symlink 要单独判断
File.GetAttributes(path).HasFlag(FileAttributes.ReparsePoint),然后决定是否存为普通文件或跳过
这些细节不会报错,但会在下游引发难以定位的问题——比如构建产物时间戳全一样,导致缓存失效;或者 Node.js 项目解压后 node_modules/.bin 里全是空目录。










