This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 0972796fa5bb CAMEL-23868: camel-file - make local work directory /
starting directory containment checks path-boundary aware (#24377)
0972796fa5bb is described below
commit 0972796fa5bbe6359ab9dfe906989674cc451803
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Jul 6 09:53:06 2026 +0200
CAMEL-23868: camel-file - make local work directory / starting directory
containment checks path-boundary aware (#24377)
The localWorkDirectory containment check
(GenericFileHelper.jailToLocalWorkDirectory,
added in CAMEL-23765) and the starting-directory check
(GenericFileProducer.jailedCheck)
both used a bare String.startsWith prefix test, which ignores path-segment
boundaries.
When the compacted directory has no trailing separator (as
jailToLocalWorkDirectory
produces via File.getPath), a sibling directory whose name merely extends
the work
directory's name (for example .../localwork versus .../localworkEVIL)
passed the check.
Introduce a shared, boundary-aware GenericFileHelper.isWithinDirectory and
route both
checks through it so they cannot diverge. It tolerates a trailing separator
(the producer
supplies one) and treats an empty directory as no boundary, preserving
existing behaviour.
Add GenericFileHelperTest coverage for a name-prefixed sibling and for the
shared helper's
boundary, trailing-separator and empty-directory cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
---
.../camel/component/file/GenericFileHelper.java | 26 +++++++++++++++++++++-
.../camel/component/file/GenericFileProducer.java | 2 +-
.../component/file/GenericFileHelperTest.java | 26 ++++++++++++++++++++++
3 files changed, 52 insertions(+), 2 deletions(-)
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 ba40b2948e78..8bb09439b40a 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
@@ -41,7 +41,7 @@ public final class GenericFileHelper {
// compact first as the remote relative name can use ../ etc
String compactTarget = FileUtil.compactPath(target.getPath());
String compactWork =
FileUtil.compactPath(localWorkDirectory.getPath());
- if (!compactTarget.startsWith(compactWork)) {
+ if (!isWithinDirectory(compactTarget, compactWork)) {
throw new GenericFileOperationFailedException(
"Cannot retrieve file to local work file: " + compactTarget
+ " as it is jailed
to the local work directory: "
@@ -49,6 +49,30 @@ public final class GenericFileHelper {
}
}
+ /**
+ * Determines whether a compacted target path is contained within a
compacted directory path, using a path-segment
+ * boundary comparison. Unlike a bare string prefix test, a sibling
directory whose name merely extends the
+ * directory name (for example {@code /work} versus {@code /workspace}) is
not considered contained. A trailing
+ * separator on the directory is tolerated, and an empty directory imposes
no boundary.
+ *
+ * @param compactTarget the compacted target path (see {@link
FileUtil#compactPath(String)})
+ * @param compactDir the compacted directory the target must stay
within
+ * @return {@code true} if the target is the directory
itself or a path inside it
+ */
+ public static boolean isWithinDirectory(String compactTarget, String
compactDir) {
+ if (compactDir.isEmpty()) {
+ // no directory boundary configured
+ return true;
+ }
+ // drop a trailing separator (if any) so the boundary comparison is
exact, regardless of whether the
+ // directory path was supplied with or without one
+ String dir = compactDir;
+ if (dir.charAt(dir.length() - 1) == File.separatorChar) {
+ dir = dir.substring(0, dir.length() - 1);
+ }
+ return compactTarget.equals(dir) || compactTarget.startsWith(dir +
File.separator);
+ }
+
public static String asExclusiveReadLockKey(GenericFile file, String key) {
// use the copy from absolute path as that was the original path of the
// file when the lock was acquired
diff --git
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileProducer.java
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileProducer.java
index 688fd4826d1e..ab50db46a208 100644
---
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileProducer.java
+++
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileProducer.java
@@ -432,7 +432,7 @@ public class GenericFileProducer<T> extends
DefaultAsyncProducer {
// first as the name can be using relative paths via ../ etc)
String compatchAnswer = FileUtil.compactPath(answer);
String compatchBaseDir = FileUtil.compactPath(baseDir);
- if (!compatchAnswer.startsWith(compatchBaseDir)) {
+ if (!GenericFileHelper.isWithinDirectory(compatchAnswer,
compatchBaseDir)) {
throw new IllegalArgumentException(
"Cannot write file with name: " + compatchAnswer
+ " as the filename is jailed
to the starting directory: "
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 5eefdbfcd907..e52a63ffa497 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
@@ -21,7 +21,9 @@ import java.io.File;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class GenericFileHelperTest {
@@ -44,5 +46,29 @@ public class GenericFileHelperTest {
() -> GenericFileHelper.jailToLocalWorkDirectory(new
File(workDir, "../../etc/passwd"), workDir));
assertThrows(GenericFileOperationFailedException.class,
() -> GenericFileHelper.jailToLocalWorkDirectory(new
File(workDir, "sub/../../escape.txt"), workDir));
+ // a sibling directory whose name merely extends the work directory
name must also be rejected
+ assertThrows(GenericFileOperationFailedException.class,
+ () -> GenericFileHelper.jailToLocalWorkDirectory(new
File(workDir, "../localworkEVIL/file.txt"), workDir));
+ }
+
+ @Test
+ public void isWithinDirectoryRespectsPathBoundaries() {
+ String sep = File.separator;
+ String work = sep + "data" + sep + "work";
+
+ // the directory itself and real children are contained
+ assertTrue(GenericFileHelper.isWithinDirectory(work, work));
+ assertTrue(GenericFileHelper.isWithinDirectory(work + sep + "a.txt",
work));
+ assertTrue(GenericFileHelper.isWithinDirectory(work + sep + "sub" +
sep + "a.txt", work));
+
+ // a trailing separator on the directory (as supplied by the file
producer) is tolerated
+ assertTrue(GenericFileHelper.isWithinDirectory(work + sep + "a.txt",
work + sep));
+
+ // a sibling whose name merely extends the directory name is NOT
contained
+ assertFalse(GenericFileHelper.isWithinDirectory(sep + "data" + sep +
"workspace" + sep + "a.txt", work));
+ assertFalse(GenericFileHelper.isWithinDirectory(sep + "data" + sep +
"workspace" + sep + "a.txt", work + sep));
+
+ // an empty directory imposes no boundary
+ assertTrue(GenericFileHelper.isWithinDirectory("anything.txt", ""));
}
}