security(import): reject path-traversal filenames in MassImportService.processRows #650
@@ -490,11 +490,18 @@ public class MassImportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Optional<File> findFileRecursive(String filename) {
|
private Optional<File> findFileRecursive(String filename) {
|
||||||
try (Stream<Path> walk = Files.walk(Paths.get(importDir))) {
|
File baseDir = new File(importDir);
|
||||||
return walk.filter(p -> !Files.isDirectory(p))
|
try (Stream<Path> walk = Files.walk(baseDir.toPath())) {
|
||||||
|
Optional<Path> match = walk.filter(p -> !Files.isDirectory(p))
|
||||||
.filter(p -> p.getFileName().toString().equals(filename))
|
.filter(p -> p.getFileName().toString().equals(filename))
|
||||||
.map(Path::toFile)
|
|
||||||
.findFirst();
|
.findFirst();
|
||||||
|
if (match.isEmpty()) return Optional.empty();
|
||||||
|
File candidate = match.get().toFile();
|
||||||
|
String baseDirCanonical = baseDir.getCanonicalPath();
|
||||||
|
if (!candidate.getCanonicalPath().startsWith(baseDirCanonical + File.separator)) {
|
||||||
|
throw DomainException.internal(ErrorCode.INTERNAL_ERROR, "Path escape detected: " + candidate);
|
||||||
|
}
|
||||||
|
return Optional.of(candidate);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -758,6 +758,21 @@ class MassImportServiceTest {
|
|||||||
.containsExactly(MassImportService.SkipReason.FILE_READ_ERROR);
|
.containsExactly(MassImportService.SkipReason.FILE_READ_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── findFileRecursive — symlink escape security regression — do not remove ─
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findFileRecursive_throwsDomainException_whenSymlinkEscapesImportDir(
|
||||||
|
@TempDir Path importDirPath, @TempDir Path outsideDir) throws Exception {
|
||||||
|
Path outsideFile = outsideDir.resolve("secret.pdf");
|
||||||
|
Files.writeString(outsideFile, "sensitive content");
|
||||||
|
Files.createSymbolicLink(importDirPath.resolve("secret.pdf"), outsideFile);
|
||||||
|
|
||||||
|
ReflectionTestUtils.setField(service, "importDir", importDirPath.toString());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> ReflectionTestUtils.invokeMethod(service, "findFileRecursive", "secret.pdf"))
|
||||||
|
.isInstanceOf(DomainException.class);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── readOds — XXE security regression ───────────────────────────────────
|
// ─── readOds — XXE security regression ───────────────────────────────────
|
||||||
|
|
||||||
// Security regression — do not remove.
|
// Security regression — do not remove.
|
||||||
|
|||||||
Reference in New Issue
Block a user