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
