修复Java路径遍历漏洞:SonarQube问题详解
2025-03-08 19:47:44
修复 SonarQube 报告的 Java 路径遍历漏洞
最近遇到了一个和路径遍历攻击相关的问题, 使用 SonarQube 进行静态代码分析时,即使我使用了 normalize()
方法,SonarQube 仍然提示存在潜在的路径遍历漏洞。真让人头疼!
代码是这样的:
private static void removeFile(MyClass someValue) {
Path filePath = Paths.get(someValue.getRootFolderPath(), someValue.getRelativePath());
if (!Files.exists(filePath)) {
LOG.warn("File does not exist", filePath.toAbsolutePath().toString());
return;
}
try {
Files.delete(filePath.getFileName());
LOG.debug("File " + someValue.getRelativePath() + " was deleted");
} catch (Exception e) {
String excMessage = "some info";
LOG.warn(excMessage, e);
}
}
SonarQube 提示:
java/nio/file/Paths.get(Ljava/lang/String;[Ljava/lang/String;)Ljava/nio/file/Path; reads a file whose location might be specified by user input
我尝试过加入 normalize()
:
Path filePath = Paths.get(someValue.getRootFolderPath(), someValue.getRelativePath()).normalize();
可还是不行! SonarQube 依然觉得有问题。 我甚至试了 FilenameUtils.getName()
的各种组合,问题依旧。看来, 得好好研究一下这个问题。
一、 问题原因
简单来说,路径遍历攻击(Path Traversal)就是攻击者通过构造恶意的路径(比如包含 ../
这种),来访问或操作预期之外的文件或目录。
原代码的问题在于, someValue.getRelativePath()
可能来自用户输入,如果用户输入了类似 ../../etc/passwd
这样的恶意路径,即使使用了normalize()
, 极端情况下, 攻击者仍有可能绕过,操作了不该操作的文件。 甚至删除一些关键系统文件。
更具体的来说, Files.delete(filePath.getFileName())
这行代码只删除了路径中的最后一个部分,它实际上删除了上级目录的一个条目,而这个上级目录是由有漏洞的路径确认的, 仍然有巨大的风险!
二、 解决方案
针对这个问题,可以试试下面这几种办法:
1. 使用 Files.deleteIfExists()
和正确的Path
修改删除逻辑,别只删除文件名, 要删除完整路径。同时使用 Files.deleteIfExists()
可以更安全地处理文件不存在的情况.
private static void removeFile(MyClass someValue) {
Path rootPath = Paths.get(someValue.getRootFolderPath());
Path filePath = rootPath.resolve(someValue.getRelativePath()).normalize();
// 额外的检查: 确保 filePath 真的是 rootPath 的子路径. 防止攻击.
if (!filePath.startsWith(rootPath)) {
LOG.warn("Invalid file path: {}", filePath.toString());
return;
}
if (!Files.exists(filePath)) {
LOG.warn("File does not exist: {}", filePath.toAbsolutePath().toString());
return;
}
try {
Files.deleteIfExists(filePath); // 使用 deleteIfExists, 更安全.
LOG.debug("File {} was deleted", filePath.toAbsolutePath());
} catch (Exception e) {
String excMessage = "some info";
LOG.warn(excMessage, e);
}
}
原理:
rootPath.resolve(someValue.getRelativePath()).normalize()
:先将相对路径解析到根路径下, 然后进行标准化, 消除..
之类的东西。filePath.startsWith(rootPath)
: 这步很关键!它确保了最终的文件路径确实是在预期的根目录下,而不是被../
这种方式跳出去了。Files.deleteIfExists(filePath)
: 更安全的删除方法。 如果文件不存在,不会抛出异常。
2. 严格校验相对路径
对someValue.getRelativePath()
进行更严格的校验, 比如只允许特定的字符, 或限制路径的层级。
private static void removeFile(MyClass someValue) {
String relativePath = someValue.getRelativePath();
// 校验相对路径,例如,只允许字母、数字、下划线和点
if (!relativePath.matches("^[a-zA-Z0-9._-]+private static void removeFile(MyClass someValue) {
String relativePath = someValue.getRelativePath();
// 校验相对路径,例如,只允许字母、数字、下划线和点
if (!relativePath.matches("^[a-zA-Z0-9._-]+$")) {
LOG.warn("Invalid relative path: {}", relativePath);
return;
}
// 也可以限制路径深度
if(relativePath.split("/").length > 3){
LOG.warn("Invalid relative path: {}, path too deep", relativePath);
return;
}
Path rootPath = Paths.get(someValue.getRootFolderPath());
Path filePath = rootPath.resolve(relativePath).normalize();
if (!filePath.startsWith(rootPath)) {
LOG.warn("Invalid file path: {}", filePath.toString());
return;
}
if (!Files.exists(filePath)) {
LOG.warn("File does not exist: {}", filePath.toAbsolutePath().toString());
return;
}
try {
Files.deleteIfExists(filePath);
LOG.debug("File {} was deleted", filePath.toAbsolutePath());
} catch (Exception e) {
String excMessage = "some info";
LOG.warn(excMessage, e);
}
}
quot;)) {
LOG.warn("Invalid relative path: {}", relativePath);
return;
}
// 也可以限制路径深度
if(relativePath.split("/").length > 3){
LOG.warn("Invalid relative path: {}, path too deep", relativePath);
return;
}
Path rootPath = Paths.get(someValue.getRootFolderPath());
Path filePath = rootPath.resolve(relativePath).normalize();
if (!filePath.startsWith(rootPath)) {
LOG.warn("Invalid file path: {}", filePath.toString());
return;
}
if (!Files.exists(filePath)) {
LOG.warn("File does not exist: {}", filePath.toAbsolutePath().toString());
return;
}
try {
Files.deleteIfExists(filePath);
LOG.debug("File {} was deleted", filePath.toAbsolutePath());
} catch (Exception e) {
String excMessage = "some info";
LOG.warn(excMessage, e);
}
}
原理:
relativePath.matches("^[a-zA-Z0-9._-]+$")
: 使用正则表达式来校验相对路径的合法性,只允许字母、数字、下划线、点和短横线。- 限制路径深度,
split("/").length > 3
:通过/
把路径拆开, 限制其深度,可以有效的减少攻击面。
3. 使用白名单
如果可能,最好是建立一个允许访问的文件名的白名单, 只有在白名单里的文件才能被删除。
private static final Set<String> ALLOWED_FILES = Set.of("file1.txt", "file2.txt", "subdir/file3.txt");
private static void removeFile(MyClass someValue) {
String relativePath = someValue.getRelativePath();
if (!ALLOWED_FILES.contains(relativePath)) {
LOG.warn("Invalid file path, not in whitelist: {}", relativePath);
return;
}
Path rootPath = Paths.get(someValue.getRootFolderPath());
Path filePath = rootPath.resolve(relativePath).normalize();
if (!filePath.startsWith(rootPath)) {
LOG.warn("Invalid file path: {}", filePath.toString());
return;
}
if (!Files.exists(filePath)) {
LOG.warn("File does not exist: {}", filePath.toAbsolutePath().toString());
return;
}
try {
Files.deleteIfExists(filePath);
LOG.debug("File {} was deleted", filePath.toAbsolutePath());
} catch (Exception e) {
String excMessage = "some info";
LOG.warn(excMessage, e);
}
}
原理:
ALLOWED_FILES
: 预先定义好一个允许删除的文件名集合。- 在删除操作进行前,检查
relativePath
是不是在这个集合里面, 只有在集合里的才允许进一步操作. 这是最安全的方式。
4. (进阶) 使用临时文件名/沙箱
更安全的做法是, 不要直接让用户传入文件名, 而是生成一个唯一的临时文件名, 让用户操作这个临时文件,完成后再根据这个临时文件名进行删除.
或者更进一步, 把文件操作限制在一个专门的“沙箱”目录里,即使出了问题,也不会影响到系统其他部分.
private static void removeFile(MyClass someValue) {
//生成唯一 ID
String fileUuid = UUID.randomUUID().toString();
//文件沙盒路径. 示例 /opt/my-app/sandbox
String sandbox = "sandbox";
//创建沙盒路径
Path sandboxPath = Paths.get(someValue.getRootFolderPath()).resolve(sandbox);
try {
if(!Files.exists(sandboxPath)){
Files.createDirectories(sandboxPath);
}
} catch (IOException e) {
LOG.error("Failed to create sandbox dir", e);
return;
}
Path filePath = sandboxPath.resolve(fileUuid);
// 假设这里有些对文件的操作...
// ....
if (!Files.exists(filePath)) {
LOG.warn("File does not exist: {}", filePath.toAbsolutePath().toString());
return;
}
try {
Files.deleteIfExists(filePath); // 再次使用 deleteIfExists
LOG.debug("File {} was deleted", filePath.toAbsolutePath());
} catch (IOException e) {
LOG.error("Failed to delete",e);
}
}
原理
- 先生成一个唯一的文件名(比如使用 UUID)
- 创建一个专门的"沙箱"文件夹,后续操作都放到沙盒路径下。
Files.createDirectories(sandboxPath)
: 确保沙箱目录存在。sandboxPath.resolve(fileUuid)
:使用沙盒目录和唯一文件名来构建安全路径.
三、 额外的安全建议
- 最小权限原则: 运行应用程序的用户应该只拥有执行必要操作的最小权限,这样可以最大限度地减少潜在的损害。
- 输入验证: 永远不要信任用户的输入, 总是要对用户的输入进行校验。
- 定期安全审计: 定期使用静态代码分析工具(比如 SonarQube)和其他安全测试工具来检查代码。
采取上边这些方法中一个或者几个,组合使用, 就可以大大降低路径遍历攻击的风险, 并让 SonarQube 满意!记住, 安全无小事,要从多个层面进行防护!