mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 12:27:53 +08:00
fix(wiki): block per-file symlink escape and stop sourcePath clobbering
This commit is contained in:
parent
f3a335f4f6
commit
4ffe7026d2
@ -133,6 +133,21 @@ public class WikiDirectoryScanService {
|
|||||||
|
|
||||||
for (Path file : files) {
|
for (Path file : files) {
|
||||||
try {
|
try {
|
||||||
|
// Per-file symlink guard: a symlinked file inside an allowed
|
||||||
|
// directory could point outside it (e.g. secret.md ->
|
||||||
|
// /etc/passwd). Resolve the real path and require it to stay
|
||||||
|
// within the validated scan root; skip escapes.
|
||||||
|
Path realFile;
|
||||||
|
try {
|
||||||
|
realFile = file.toRealPath();
|
||||||
|
} catch (IOException e) {
|
||||||
|
realFile = file.toAbsolutePath().normalize();
|
||||||
|
}
|
||||||
|
if (!realFile.startsWith(dir)) {
|
||||||
|
errors.add("Skipped symlink escaping the scan root: " + file.getFileName());
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
String absolutePath = file.toAbsolutePath().normalize().toString();
|
String absolutePath = file.toAbsolutePath().normalize().toString();
|
||||||
String fileName = file.getFileName().toString();
|
String fileName = file.getFileName().toString();
|
||||||
String ext = getExtension(fileName);
|
String ext = getExtension(fileName);
|
||||||
|
|||||||
@ -106,7 +106,10 @@ public class WikiRawMaterialService {
|
|||||||
// addText dedups internally by hash, so this reuses sameContent when
|
// addText dedups internally by hash, so this reuses sameContent when
|
||||||
// unchanged and inserts + triggers processing when the content differs.
|
// unchanged and inserts + triggers processing when the content differs.
|
||||||
WikiRawMaterialEntity raw = addText(kbId, fileName, content);
|
WikiRawMaterialEntity raw = addText(kbId, fileName, content);
|
||||||
if (raw != null) {
|
// Only stamp the path on a genuinely new raw. When the content matched
|
||||||
|
// an existing raw (possibly a different file with identical content),
|
||||||
|
// overwriting its sourcePath would corrupt that raw's provenance.
|
||||||
|
if (raw != null && sameContent == null) {
|
||||||
updateSourcePath(raw.getId(), absolutePath);
|
updateSourcePath(raw.getId(), absolutePath);
|
||||||
}
|
}
|
||||||
return sameContent == null;
|
return sameContent == null;
|
||||||
|
|||||||
@ -33,6 +33,8 @@ class WikiSourceWatcherServiceE2ETest {
|
|||||||
private WikiSourceWatcherService watcherService;
|
private WikiSourceWatcherService watcherService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private WikiKnowledgeBaseService kbService;
|
private WikiKnowledgeBaseService kbService;
|
||||||
|
@Autowired
|
||||||
|
private WikiDirectoryScanService scanService;
|
||||||
|
|
||||||
private static final java.util.concurrent.atomic.AtomicLong SEQ =
|
private static final java.util.concurrent.atomic.AtomicLong SEQ =
|
||||||
new java.util.concurrent.atomic.AtomicLong(System.nanoTime());
|
new java.util.concurrent.atomic.AtomicLong(System.nanoTime());
|
||||||
@ -64,6 +66,27 @@ class WikiSourceWatcherServiceE2ETest {
|
|||||||
assertEquals(1, afterEdit, "a modified file must be re-ingested");
|
assertEquals(1, afterEdit, "a modified file must be re-ingested");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void symlinkFileEscapingScanRoot_isNotIngested(@TempDir Path sourceDir, @TempDir Path outside)
|
||||||
|
throws java.io.IOException {
|
||||||
|
Files.writeString(sourceDir.resolve("real.md"), "# Real\n\nlocal content");
|
||||||
|
Path secret = Files.writeString(outside.resolve("secret.md"), "TOP SECRET OUTSIDE");
|
||||||
|
Path link = sourceDir.resolve("leak.md");
|
||||||
|
try {
|
||||||
|
Files.createSymbolicLink(link, secret);
|
||||||
|
} catch (UnsupportedOperationException | java.io.IOException e) {
|
||||||
|
return; // filesystem without symlink support — skip
|
||||||
|
}
|
||||||
|
|
||||||
|
long kb = SEQ.incrementAndGet();
|
||||||
|
WikiDirectoryScanService.ScanResult result = scanService.scanDirectory(kb, sourceDir.toString());
|
||||||
|
|
||||||
|
// Only the real file is ingested; the symlink escaping the root is skipped.
|
||||||
|
assertEquals(1, result.added(), "symlinked file pointing outside the root must not be ingested");
|
||||||
|
assertTrue(result.skipped() >= 1 || !result.errors().isEmpty(),
|
||||||
|
"the escaping symlink should be reported as skipped");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void kbsWithoutSourceDirectory_areSkipped() {
|
void kbsWithoutSourceDirectory_areSkipped() {
|
||||||
// A KB with no source directory must not cause errors in the cycle.
|
// A KB with no source directory must not cause errors in the cycle.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user