This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.22.x by this push:
new e11eddaddc24 CAMEL-24627: resolve symlinks when containing local work
directory downloads
e11eddaddc24 is described below
commit e11eddaddc247372ff2687b000273051d7a42380
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Sep 5 22:22:10 2026 +0200
CAMEL-24627: resolve symlinks when containing local work directory downloads
Backport of #26121 to camel-4.22.x.
GenericFileHelper.jailToLocalWorkDirectory performed a lexical path-boundary
check only. A symbolic link inside the work directory that resolves outside
of it could therefore be used to write files elsewhere on the filesystem.
After the lexical check, existing filesystem path segments are now resolved
(following symbolic links) and the containment is re-verified. Valid nested
download paths continue to work unchanged. Affects camel-file, camel-ftp,
camel-smb, camel-mina-sftp and camel-azure-files.
Closes #26138
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
.../camel/component/file/GenericFileHelper.java | 41 ++++++++++++++++++++++
.../component/file/GenericFileHelperTest.java | 29 +++++++++++++++
2 files changed, 70 insertions(+)
diff --git
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileHelper.java
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileHelper.java
index ff3957e296e5..278a93c88441 100644
---
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileHelper.java
+++
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileHelper.java
@@ -17,6 +17,10 @@
package org.apache.camel.component.file;
import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
import java.util.function.Supplier;
import org.apache.camel.Exchange;
@@ -47,6 +51,43 @@ public final class GenericFileHelper {
+ " as it is jailed
to the local work directory: "
+ compactWork);
}
+ // defense-in-depth: the lexical check above cannot see symbolic
links, so also resolve the existing
+ // filesystem path segments (following links) and re-check. This
prevents a symbolic link inside the
+ // local work directory that resolves outside of it from being used to
write files elsewhere. Valid
+ // nested paths continue to work unchanged. Skipped when no work
directory boundary is configured.
+ if (!compactWork.isEmpty()) {
+ try {
+ Path resolvedWork =
resolveExistingPathSegments(localWorkDirectory.toPath());
+ Path resolvedTarget =
resolveExistingPathSegments(target.toPath());
+ if (!resolvedTarget.startsWith(resolvedWork)) {
+ throw new GenericFileOperationFailedException(
+ "Cannot retrieve file to local work file: " +
compactTarget
+ + " as it is
jailed to the local work directory: "
+ +
compactWork);
+ }
+ } catch (IOException e) {
+ throw new GenericFileOperationFailedException(
+ "Cannot verify local work file: " + compactTarget
+ + " is within
the local work directory: " + compactWork,
+ e);
+ }
+ }
+ }
+
+ private static Path resolveExistingPathSegments(Path path) throws
IOException {
+ // Preserve the raw path segments here. Normalizing before resolving
links changes the filesystem meaning of
+ // paths such as link/../file when link points to another directory.
+ final Path absolutePath = path.toAbsolutePath();
+ Path existingPath = absolutePath;
+ while (existingPath != null && !Files.exists(existingPath,
LinkOption.NOFOLLOW_LINKS)) {
+ existingPath = existingPath.getParent();
+ }
+ if (existingPath == null) {
+ throw new IOException("No existing ancestor found for " + path);
+ }
+
+ final Path resolvedExistingPath = existingPath.toRealPath();
+ return
resolvedExistingPath.resolve(existingPath.relativize(absolutePath)).normalize();
}
/**
diff --git
a/components/camel-file/src/test/java/org/apache/camel/component/file/GenericFileHelperTest.java
b/components/camel-file/src/test/java/org/apache/camel/component/file/GenericFileHelperTest.java
index 2b6c29d10b79..65bbb25d30ed 100644
---
a/components/camel-file/src/test/java/org/apache/camel/component/file/GenericFileHelperTest.java
+++
b/components/camel-file/src/test/java/org/apache/camel/component/file/GenericFileHelperTest.java
@@ -17,8 +17,13 @@
package org.apache.camel.component.file;
import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -51,6 +56,30 @@ public class GenericFileHelperTest {
() -> GenericFileHelper.jailToLocalWorkDirectory(new
File(workDir, "../localworkEVIL/file.txt"), workDir));
}
+ @Test
+ public void shouldRejectSymlinkEscapingLocalWorkDirectory(@TempDir Path
tmp) throws IOException {
+ Path work = Files.createDirectories(tmp.resolve("work"));
+ Path outside = Files.createDirectories(tmp.resolve("outside"));
+
+ // a symbolic link inside the work directory that points outside of it
+ Path link = work.resolve("link");
+ try {
+ Files.createSymbolicLink(link, outside);
+ } catch (UnsupportedOperationException | IOException e) {
+ Assumptions.abort("Symbolic links are not supported on this
platform: " + e.getMessage());
+ }
+
+ // a file written through the symlink resolves outside the work
directory and must be rejected, even though
+ // it passes a lexical-only containment check
+ File escaping = new File(link.toFile(), "evil.txt");
+ assertThrows(GenericFileOperationFailedException.class,
+ () -> GenericFileHelper.jailToLocalWorkDirectory(escaping,
work.toFile()));
+
+ // a legitimate file within a real (not-yet-existing) subdirectory of
the work directory is still allowed
+ File legit = new File(work.toFile(), "sub/ok.txt");
+ assertDoesNotThrow(() ->
GenericFileHelper.jailToLocalWorkDirectory(legit, work.toFile()));
+ }
+
@Test
public void isWithinDirectoryRespectsPathBoundaries() {
String sep = File.separator;