This is an automated email from the ASF dual-hosted git repository.

oscerd pushed a commit to branch fix/CAMEL-23765
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 3cdac3a60c69a7e98bcf279b758a5388e54aedfc
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Jun 22 13:35:20 2026 +0200

    CAMEL-23765: remote-file consumers - contain localWorkDirectory downloads 
within the work directory
    
    When localWorkDirectory was enabled, the remote-file consumers built the 
local
    work file path from the remote file name (target.getRelativeFilePath()) 
without
    ensuring the result stayed within the configured work directory. A remote 
file
    name containing ../ sequences could therefore resolve to a path outside the 
work
    directory (arbitrary local file write), unlike the file producer which 
already
    jails writes via FileUtil.compactPath + startsWith when 
jailStartingDirectory is
    enabled.
    
    This adds a shared GenericFileHelper.jailToLocalWorkDirectory containment 
check,
    mirroring the producer, and applies it (for both the in-progress temp file 
and the
    final file) in the localWorkDirectory download path of FtpOperations, 
SftpOperations,
    MinaSftpOperations (camel-mina-sftp), FilesOperations (camel-azure-files) 
and
    SmbOperations (camel-smb). The check reuses the existing 
jailStartingDirectory option
    (default true), so it is secure by default and can be disabled with
    jailStartingDirectory=false. A remote file resolving outside the work 
directory is
    rejected with a GenericFileOperationFailedException.
    
    Adds GenericFileHelperTest and a 4.21 upgrade-guide note.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---
 .../component/file/azure/FilesOperations.java      |  8 ++++
 .../camel/component/file/GenericFileHelper.java    | 23 +++++++++++
 .../component/file/GenericFileHelperTest.java      | 48 ++++++++++++++++++++++
 .../camel/component/file/remote/FtpOperations.java |  8 ++++
 .../component/file/remote/SftpOperations.java      |  8 ++++
 .../file/remote/mina/MinaSftpOperations.java       |  8 ++++
 .../apache/camel/component/smb/SmbOperations.java  |  8 ++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_21.adoc    |  9 ++++
 8 files changed, 120 insertions(+)

diff --git 
a/components/camel-azure/camel-azure-files/src/main/java/org/apache/camel/component/file/azure/FilesOperations.java
 
b/components/camel-azure/camel-azure-files/src/main/java/org/apache/camel/component/file/azure/FilesOperations.java
index d82f4dbd5a1c..28a8b2ea87bc 100644
--- 
a/components/camel-azure/camel-azure-files/src/main/java/org/apache/camel/component/file/azure/FilesOperations.java
+++ 
b/components/camel-azure/camel-azure-files/src/main/java/org/apache/camel/component/file/azure/FilesOperations.java
@@ -47,6 +47,7 @@ import org.apache.camel.component.file.FileComponent;
 import org.apache.camel.component.file.GenericFile;
 import org.apache.camel.component.file.GenericFileEndpoint;
 import org.apache.camel.component.file.GenericFileExist;
+import org.apache.camel.component.file.GenericFileHelper;
 import org.apache.camel.component.file.GenericFileOperationFailedException;
 import org.apache.camel.component.file.remote.RemoteFile;
 import org.apache.camel.component.file.remote.RemoteFileConfiguration;
@@ -302,9 +303,16 @@ public class FilesOperations extends NormalizedOperations {
                     "Exchange should have the " + 
FileComponent.FILE_EXCHANGE_FILE + " set");
             String relativeName = target.getRelativeFilePath();
 
+            File localWorkDir = local;
             inProgress = new File(local, relativeName + ".inprogress");
             local = new File(local, relativeName);
 
+            // ensure the local work file stays within the local work 
directory (CAMEL-23765)
+            if (endpoint.isJailStartingDirectory()) {
+                GenericFileHelper.jailToLocalWorkDirectory(inProgress, 
localWorkDir);
+                GenericFileHelper.jailToLocalWorkDirectory(local, 
localWorkDir);
+            }
+
             // create directory to local work file
             boolean result = local.mkdirs();
             if (!result) {
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 b86351c010d6..ba40b2948e78 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
@@ -16,16 +16,39 @@
  */
 package org.apache.camel.component.file;
 
+import java.io.File;
 import java.util.function.Supplier;
 
 import org.apache.camel.Exchange;
 import org.apache.camel.support.MessageHelper;
+import org.apache.camel.util.FileUtil;
 
 public final class GenericFileHelper {
 
     private GenericFileHelper() {
     }
 
+    /**
+     * Ensures the resolved local work file stays within the configured local 
work directory. The remote file name used
+     * to build the local work file path may contain {@code ../} sequences 
that would otherwise resolve to a path
+     * outside the work directory.
+     *
+     * @param  target                              the resolved local work 
file (or its in-progress temp file)
+     * @param  localWorkDirectory                  the local work directory 
the file must stay within
+     * @throws GenericFileOperationFailedException if the target resolves 
outside the local work directory
+     */
+    public static void jailToLocalWorkDirectory(File target, File 
localWorkDirectory) {
+        // 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)) {
+            throw new GenericFileOperationFailedException(
+                    "Cannot retrieve file to local work file: " + compactTarget
+                                                          + " as it is jailed 
to the local work directory: "
+                                                          + compactWork);
+        }
+    }
+
     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/test/java/org/apache/camel/component/file/GenericFileHelperTest.java
 
b/components/camel-file/src/test/java/org/apache/camel/component/file/GenericFileHelperTest.java
new file mode 100644
index 000000000000..5eefdbfcd907
--- /dev/null
+++ 
b/components/camel-file/src/test/java/org/apache/camel/component/file/GenericFileHelperTest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.file;
+
+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.assertThrows;
+
+public class GenericFileHelperTest {
+
+    private final File workDir = new File("target/localwork");
+
+    @Test
+    public void shouldAllowFilesWithinLocalWorkDirectory() {
+        // a plain name, a nested name, and a ../ that still resolves within 
the work directory are all allowed
+        assertDoesNotThrow(() -> 
GenericFileHelper.jailToLocalWorkDirectory(new File(workDir, "file.txt"), 
workDir));
+        assertDoesNotThrow(() -> 
GenericFileHelper.jailToLocalWorkDirectory(new File(workDir, 
"sub/dir/file.txt"), workDir));
+        assertDoesNotThrow(() -> 
GenericFileHelper.jailToLocalWorkDirectory(new File(workDir, 
"sub/../file.txt"), workDir));
+    }
+
+    @Test
+    public void shouldRejectFilesEscapingLocalWorkDirectory() {
+        // a remote file name that resolves outside the configured local work 
directory must be rejected
+        assertThrows(GenericFileOperationFailedException.class,
+                () -> GenericFileHelper.jailToLocalWorkDirectory(new 
File(workDir, "../escape.txt"), workDir));
+        assertThrows(GenericFileOperationFailedException.class,
+                () -> GenericFileHelper.jailToLocalWorkDirectory(new 
File(workDir, "../../etc/passwd"), workDir));
+        assertThrows(GenericFileOperationFailedException.class,
+                () -> GenericFileHelper.jailToLocalWorkDirectory(new 
File(workDir, "sub/../../escape.txt"), workDir));
+    }
+}
diff --git 
a/components/camel-ftp/src/main/java/org/apache/camel/component/file/remote/FtpOperations.java
 
b/components/camel-ftp/src/main/java/org/apache/camel/component/file/remote/FtpOperations.java
index 2f7df650e63d..0b942b47248a 100644
--- 
a/components/camel-ftp/src/main/java/org/apache/camel/component/file/remote/FtpOperations.java
+++ 
b/components/camel-ftp/src/main/java/org/apache/camel/component/file/remote/FtpOperations.java
@@ -32,6 +32,7 @@ import org.apache.camel.component.file.FileComponent;
 import org.apache.camel.component.file.GenericFile;
 import org.apache.camel.component.file.GenericFileEndpoint;
 import org.apache.camel.component.file.GenericFileExist;
+import org.apache.camel.component.file.GenericFileHelper;
 import org.apache.camel.component.file.GenericFileOperationFailedException;
 import org.apache.camel.support.ObjectHelper;
 import org.apache.camel.support.task.BlockingTask;
@@ -525,9 +526,16 @@ public class FtpOperations implements 
RemoteFileOperations<FTPFile> {
                     "Exchange should have the " + 
FileComponent.FILE_EXCHANGE_FILE + " set");
             String relativeName = target.getRelativeFilePath();
 
+            File localWorkDir = local;
             temp = new File(local, relativeName + ".inprogress");
             local = new File(local, relativeName);
 
+            // ensure the local work file stays within the local work 
directory (CAMEL-23765)
+            if (endpoint.isJailStartingDirectory()) {
+                GenericFileHelper.jailToLocalWorkDirectory(temp, localWorkDir);
+                GenericFileHelper.jailToLocalWorkDirectory(local, 
localWorkDir);
+            }
+
             // create directory to local work file
             boolean result = local.mkdirs();
             if (!result) {
diff --git 
a/components/camel-ftp/src/main/java/org/apache/camel/component/file/remote/SftpOperations.java
 
b/components/camel-ftp/src/main/java/org/apache/camel/component/file/remote/SftpOperations.java
index adc1fbafb93f..56492191b0c9 100644
--- 
a/components/camel-ftp/src/main/java/org/apache/camel/component/file/remote/SftpOperations.java
+++ 
b/components/camel-ftp/src/main/java/org/apache/camel/component/file/remote/SftpOperations.java
@@ -57,6 +57,7 @@ import org.apache.camel.component.file.FileComponent;
 import org.apache.camel.component.file.GenericFile;
 import org.apache.camel.component.file.GenericFileEndpoint;
 import org.apache.camel.component.file.GenericFileExist;
+import org.apache.camel.component.file.GenericFileHelper;
 import org.apache.camel.component.file.GenericFileOperationFailedException;
 import org.apache.camel.spi.CamelLogger;
 import org.apache.camel.support.ResourceHelper;
@@ -1037,9 +1038,16 @@ public class SftpOperations implements 
RemoteFileOperations<SftpRemoteFile> {
             // use relative filename in local work directory
             String relativeName = file.getRelativeFilePath();
 
+            File localWorkDir = local;
             temp = new File(local, relativeName + ".inprogress");
             local = new File(local, relativeName);
 
+            // ensure the local work file stays within the local work 
directory (CAMEL-23765)
+            if (endpoint.isJailStartingDirectory()) {
+                GenericFileHelper.jailToLocalWorkDirectory(temp, localWorkDir);
+                GenericFileHelper.jailToLocalWorkDirectory(local, 
localWorkDir);
+            }
+
             // create directory to local work file
             local.mkdirs();
 
diff --git 
a/components/camel-mina-sftp/src/main/java/org/apache/camel/component/file/remote/mina/MinaSftpOperations.java
 
b/components/camel-mina-sftp/src/main/java/org/apache/camel/component/file/remote/mina/MinaSftpOperations.java
index 30dc60a9c4b8..8b6613c3e9e7 100644
--- 
a/components/camel-mina-sftp/src/main/java/org/apache/camel/component/file/remote/mina/MinaSftpOperations.java
+++ 
b/components/camel-mina-sftp/src/main/java/org/apache/camel/component/file/remote/mina/MinaSftpOperations.java
@@ -42,6 +42,7 @@ import org.apache.camel.component.file.FileComponent;
 import org.apache.camel.component.file.GenericFile;
 import org.apache.camel.component.file.GenericFileEndpoint;
 import org.apache.camel.component.file.GenericFileExist;
+import org.apache.camel.component.file.GenericFileHelper;
 import org.apache.camel.component.file.GenericFileOperationFailedException;
 import org.apache.camel.component.file.remote.FtpConstants;
 import org.apache.camel.component.file.remote.RemoteFile;
@@ -1225,8 +1226,15 @@ public class MinaSftpOperations implements 
RemoteFileOperations<SftpRemoteFile>
 
         try {
             String relativeName = file.getRelativeFilePath();
+            File localWorkDir = local;
             temp = new File(local, relativeName + ".inprogress");
             local = new File(local, relativeName);
+
+            // ensure the local work file stays within the local work 
directory (CAMEL-23765)
+            if (endpoint.isJailStartingDirectory()) {
+                GenericFileHelper.jailToLocalWorkDirectory(temp, localWorkDir);
+                GenericFileHelper.jailToLocalWorkDirectory(local, 
localWorkDir);
+            }
             local.mkdirs();
 
             if (temp.exists()) {
diff --git 
a/components/camel-smb/src/main/java/org/apache/camel/component/smb/SmbOperations.java
 
b/components/camel-smb/src/main/java/org/apache/camel/component/smb/SmbOperations.java
index 69f46d68d08e..ee6e96aefb39 100644
--- 
a/components/camel-smb/src/main/java/org/apache/camel/component/smb/SmbOperations.java
+++ 
b/components/camel-smb/src/main/java/org/apache/camel/component/smb/SmbOperations.java
@@ -46,6 +46,7 @@ import org.apache.camel.component.file.FileComponent;
 import org.apache.camel.component.file.GenericFile;
 import org.apache.camel.component.file.GenericFileEndpoint;
 import org.apache.camel.component.file.GenericFileExist;
+import org.apache.camel.component.file.GenericFileHelper;
 import org.apache.camel.component.file.GenericFileOperationFailedException;
 import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.IOHelper;
@@ -337,12 +338,19 @@ public class SmbOperations implements SmbFileOperations {
             // use relative filename in local work directory
             String relativeName = file.getRelativeFilePath();
 
+            java.io.File localWorkDir = local;
             temp = new java.io.File(local, relativeName + ".inprogress");
 
             // create directory to local work file
             local.mkdirs();
             local = new java.io.File(local, relativeName);
 
+            // ensure the local work file stays within the local work 
directory (CAMEL-23765)
+            if (endpoint.isJailStartingDirectory()) {
+                GenericFileHelper.jailToLocalWorkDirectory(temp, localWorkDir);
+                GenericFileHelper.jailToLocalWorkDirectory(local, 
localWorkDir);
+            }
+
             // delete any existing files
             if (temp.exists()) {
                 if (!FileUtil.deleteFile(temp)) {
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
index eb06df2d01e4..3a9bb921062e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
@@ -1036,6 +1036,15 @@ its signature because no 
`validateSigningCertificateChain` is configured. A new
 unverifiable signed messages with an `insufficient-message-security` error 
instead of delivering them
 unverified. The default behaviour is otherwise unchanged.
 
+=== camel-ftp, camel-sftp, camel-mina-sftp, camel-azure-files, camel-smb
+
+When `localWorkDirectory` is used, the remote-file consumers now ensure the 
downloaded local work file
+stays within the configured work directory, so a remote file name containing 
`../` sequences can no longer
+resolve to a path outside it. The containment check honours the existing 
`jailStartingDirectory` option
+(default `true`), consistent with the file producer; set 
`jailStartingDirectory=false` to disable it. A
+remote file that resolves outside the local work directory is now rejected 
with a
+`GenericFileOperationFailedException`.
+
 === camel-oauth
 
 `OAuthTokenRequest.refreshTokenGrant(...)` now sends the RFC 6749 
`refresh_token` form parameter for

Reply via email to