task-151: 路径安全校验(PathSafetyGuard 规范化 startsWith 判定、../与绝对路径与符号链接逃逸拒绝、接入清理路径)+ 8 条测试

This commit is contained in:
2026-09-02 06:24:37 +08:00
parent ffc3963345
commit f0e12b9b1b
3 changed files with 159 additions and 0 deletions
@@ -57,6 +57,11 @@ public class LocalTempCleanupService {
for (File child : children) {
try {
// 路径安全:child 必须位于临时根目录内(防穿越/符号链接逃逸),否则跳过并告警
if (!PathSafetyGuard.isInside(tempDir, child)) {
log.warn("local temp cleanup skipped unsafe path: {}", child.getAbsolutePath());
continue;
}
if (child.isFile() && isManagedRootTempFile(child) && isExpired(child, sourceExpireBefore)) {
if (FileUtil.del(child)) {
deletedSourceCount++;
@@ -89,6 +94,9 @@ public class LocalTempCleanupService {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
if (!PathSafetyGuard.isInside(file, child)) {
continue;
}
deletedCount += deleteExpiredChildrenRecursively(child, expireBefore);
}
}
@@ -0,0 +1,54 @@
package com.nanri.aiimage.modules.file.service;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
/**
* 临时文件删除前的路径安全校验(task-151)。
*
* 删除前校验:child 规范化后必须位于 root 内(startsWith(root) 且不等于 root);
* 穿越(../、绝对路径逃逸、符号链接逃逸)拒绝;null/非法输入返回 false。
* 符号链接用 toRealPath 解析(存在时),防止链接指向根外文件被误删。
*/
public final class PathSafetyGuard {
private PathSafetyGuard() {
}
/**
* child 是否位于 root 内(规范化比较;root 本身不算内部)。
* 路径不存在时以 toAbsolutePath().normalize() 兜底,存在时优先 toRealPath
* 解析符号链接。
*/
public static boolean isInside(File root, File child) {
if (root == null || child == null) {
return false;
}
Path rootPath = resolve(root);
Path childPath = resolve(child);
if (rootPath == null || childPath == null) {
return false;
}
return childPath.startsWith(rootPath) && !childPath.equals(rootPath);
}
/** 路径名是否含穿越特征(../、绝对路径、盘符、反斜杠)。 */
public static boolean isTraversal(String name) {
if (name == null || name.isBlank()) {
return true;
}
return name.contains("..")
|| name.startsWith("/")
|| name.matches("^[A-Za-z]:.*")
|| name.contains("\\");
}
private static Path resolve(File file) {
try {
return file.toPath().toRealPath();
} catch (IOException ex) {
return file.toPath().toAbsolutePath().normalize();
}
}
}