From ddf301d356362e83e0820e182c0d6608359bf83e Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Wed, 24 Jun 2026 11:48:28 +0200 Subject: [PATCH] CAMEL-23765: remote-file consumers - contain localWorkDirectory downloads within the work directory Backport of #24180 to camel-4.14.x. When localWorkDirectory was enabled, the remote-file consumers built the local work file path from the remote file name without ensuring the result stayed within the configured work directory, so a remote file name containing ../ sequences could resolve to a path outside it. A shared GenericFileHelper.jailToLocalWorkDirectory containment check (compactPath + startsWith, mirroring the file producer's jail) is now applied to both the in-progress temp file and the final file, before mkdirs, in FtpOperations, SftpOperations, FilesOperations and SmbOperations. It reuses the existing jailStartingDirectory option (default true). Note: camel-mina-sftp does not exist on camel-4.14.x (added later), so its MinaSftpOperations change from the main PR is not applicable here. Source-only backport: the jailStartingDirectory producer->common label change and its regenerated metadata are intentionally omitted; the upgrade-guide entry lives on main. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../component/file/azure/FilesOperations.java | 8 ++++ .../component/file/GenericFileHelper.java | 23 +++++++++ .../component/file/GenericFileHelperTest.java | 48 +++++++++++++++++++ .../component/file/remote/FtpOperations.java | 8 ++++ .../component/file/remote/SftpOperations.java | 8 ++++ .../camel/component/smb/SmbOperations.java | 12 ++++- 6 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 components/camel-file/src/test/java/org/apache/camel/component/file/GenericFileHelperTest.java 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 d669187fbbe67..423d03efdaeda 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.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; @@ -301,9 +302,16 @@ private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exc "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 b86351c010d60..ba40b2948e78e 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 0000000000000..5eefdbfcd9076 --- /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 d1dc62fcb5c89..55055192873ea 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.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 @@ private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exc "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 76b40e95de385..f661f8c9ea481 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 @@ -55,6 +55,7 @@ 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; @@ -935,9 +936,16 @@ private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exc // 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-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 ba038a897233d..5b37affa2ef0d 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 @@ -44,6 +44,7 @@ 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; @@ -291,11 +292,18 @@ private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exc // use relative filename in local work directory String relativeName = file.getRelativeFilePath(); + java.io.File localWorkDir = local; temp = new java.io.File(local, relativeName + ".inprogress"); + 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); + } // create directory to local work file - local.mkdirs(); - local = new java.io.File(local, relativeName); + localWorkDir.mkdirs(); // delete any existing files if (temp.exists()) {