pathinfo()只返回gz,因其严格按最后一个点后字符串定义扩展名,属posix兼容设计;archive.tar.gz中.gz是最后一段,tar被视为文件名部分;多级扩展名需手动截取,但上传校验不应依赖此逻辑。

当你用 pathinfo($filename, PATHINFO_EXTENSION) 处理 archive.tar.gz 这类文件时,它只返回 gz,而不是你预期的 tar.gz,这是因为 PHP 的 pathinfo() 函数严格按“最后一个点之后的部分”定义扩展名,不识别复合归档格式语义,也不做业务层约定解析。
pathinfo 为什么只取最后一段扩展名
PHP 的 pathinfo() 在设计上把扩展名定义为“路径中最后一个点(.)右侧直到结尾或路径分隔符的字符串”,这是 POSIX 兼容的底层逻辑,不是 bug,而是明确行为。
对 archive.tar.gz:最后一个点在 .gz 前,所以 【PATHINFO_EXTENSION 只返回 gz】;tar 被当作文件名的一部分,由 PATHINFO_FILENAME 返回为 archive.tar。
这种设计避免了歧义——比如 config.ini.bak,若返回 ini.bak 就无法区分是备份文件还是 INI 格式文件;统一取最后一段,语义清晰、可预测。
哪些情况会触发只返回单段扩展名
方法一:含多个点的常规文件名
report.v2.final.pdf → extension 是 pdf,不是 v2.final.pdf 或 final.pdf。
方法二:Unix 隐藏文件
.gitignore → extension 是 gitignore(因为 basename 以点开头,整段被当作 extension),filename 为空字符串。
方法三:末尾带点的路径
/var/log/app. → extension 是空字符串(最后一个点后无字符),但 /var/log/app.log. 会返回空字符串而非 log. —— 点后必须有非分隔符内容才被识别。
如何拿到完整的多级扩展名(如 tar.gz)
第一步:先用 pathinfo($filename, PATHINFO_FILENAME) 拿到不带扩展名的主体部分
第二步:从原始字符串中截取从第一个点开始、到最后一段扩展名前的所有内容
例如:$filename = 'archive.tar.gz'; $base = pathinfo($filename, PATHINFO_FILENAME); // 'archive.tar' $ext = pathinfo($filename, PATHINFO_EXTENSION); // 'gz' $full_ext = substr($filename, strlen($base) + 1); // 'tar.gz'
注意:【此逻辑仅适用于已知格式的文件,不能泛化为通用规则】,因为 image.jpeg.backup 和 data.json.zip 的“完整扩展名”含义完全不同,需结合业务定义。
上传校验时该不该依赖 tar.gz 这种复合扩展名
不应该。
攻击者可构造 shell.php.tar.gz 上传,pathinfo() 返回 gz,若你白名单包含 gz 就放行——但解压后实际是 PHP 文件;反之,若你强行匹配 tar.gz,又会误拦合法的 doc.pdf.gz。
真正安全的做法是:只校验最后一段扩展名(gz),再配合 MIME 类型检测 + 解压后二次扫描内容,而不是在扩展名层面模拟归档语义。










