String.endsWith() 不能安全校验文件扩展名,因不区分大小写、无法识别恶意多扩展名(如"malicious.jpg.exe")且易受路径中点号干扰;须先用lastIndexOf(".")提取真实扩展名并转小写,再白名单校验。

String.endsWith() 本身不能安全校验文件扩展名
直接用 String.endsWith() 判断扩展名(比如 filename.endsWith(".jpg"))看似简单,但极易出错:它不区分大小写、不验证扩展名是否在文件名末尾真实存在("malicious.jpg.exe" 也会通过 .endsWith(".jpg")),更无法处理路径中含点号的场景(如 "/home/user/my.file.txt")。浏览器上传的 File.name 或后端收到的原始文件名,都可能被恶意构造或包含多层路径信息。
必须先提取真实扩展名再比对
核心是「分离扩展名」——不是字符串后缀匹配,而是按最后一个 . 拆分,并确保该 . 出现在合法位置(即不在开头、不连续、且后面还有字符)。推荐做法:
- 用
lastIndexOf(".")找最后一个点的位置 - 检查该位置是否 > 0(排除
".gitignore"这类隐藏文件误判)且lastIndex != filename.length - 1(排除以点结尾) - 截取
filename.substring(lastIndex + 1)得到小写扩展名(如"JPG"→"jpg") - 用
Set.of("jpg", "png", "pdf").contains(ext)或Arrays.asList(...).contains()做白名单校验
示例片段:
String ext = "";
int dotIndex = filename.lastIndexOf(".");
if (dotIndex > 0 && dotIndex < filename.length() - 1) {
ext = filename.substring(dotIndex + 1).toLowerCase();
}
boolean allowed = Set.of("jpg", "jpeg", "png", "pdf").contains(ext);注意前端校验仅作辅助,后端必须重复验证
前端用 String.endsWith() 做即时提示(如“仅支持 JPG/PNG”)可以提升体验,但完全不可信。攻击者可绕过 JS 直接发请求;浏览器也可能修改 File.name(如 Safari 对中文路径的处理)。后端收到的原始文件名、Content-Type、甚至文件魔数(magic bytes)都应参与判断。尤其当使用 MultipartFile.getOriginalFilename()(Spring)或 req.files?.file?.name(Express)时,必须重新提取扩展名——不能依赖前端传来的 ext 字段。
Node.js 和 Java 中的典型陷阱
Node.js 的 path.extname() 看似方便,但它会返回带点的扩展名(如 ".jpg"),直接用于 Set.contains() 会失败;Java 的 FilenameUtils.getExtension()(Apache Commons)默认不转小写,且对 "archive.tar.gz" 返回 "gz" 而非 "tar.gz"。这些细节都会导致白名单失效。最稳妥的方式仍是手动用 lastIndexOf(".") + substring() 提取纯扩展名字符串,并统一转小写后再比对。
真正关键的不是“怎么调用 endsWith”,而是“怎么拿到那个值得比对的扩展名字符串”。漏掉路径清理、大小写归一或边界检查,再短的代码也等于没校验。

















