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

Croway pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.18.x by this push:
     new 3c322a1419c9 CAMEL-24548 CAMEL-24549: Harden cloud storage download 
containment
3c322a1419c9 is described below

commit 3c322a1419c9037c4b3aa479d180114db0b2a7f9
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 1 07:38:03 2026 +0200

    CAMEL-24548 CAMEL-24549: Harden cloud storage download containment
    
    Backport of #25873 to camel-4.18.x. Local downloads in
    camel-azure-storage-blob, camel-azure-storage-datalake, and
    camel-google-storage now resolve existing filesystem path segments
    before checking that the destination stays inside the configured
    directory, rejecting a symbolic link inside the download directory
    that resolves outside of it.
    
    This branch predates the shared camel-azure-common module and
    AzureFileNameHelper, so the Azure fix is applied directly to the
    duplicated resolveWithinDirectory() methods in BlobOperations and
    DataLakeFileOperations instead. The Google fix is a direct port,
    since GoogleCloudStorageFileNameHelper is unchanged from main. The
    upgrade-guide note was added to the existing "Upgrading from 4.18.4
    to 4.18.5" section of camel-4x-upgrade-guide-4_18.adoc.
    
    Co-authored-by: Codex <[email protected]>
    Co-authored-by: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../storage/blob/operations/BlobOperations.java    | 39 +++++++++++++++++++-
 .../blob/operations/BlobOperationsTest.java        | 21 +++++++++++
 .../operations/DataLakeFileOperations.java         | 39 +++++++++++++++++++-
 .../operations/DataLakeFileOperationTest.java      | 21 +++++++++++
 .../storage/GoogleCloudStorageFileNameHelper.java  | 43 ++++++++++++++++++++--
 .../GoogleCloudStorageFileNameHelperTest.java      | 31 ++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_18.adoc    | 13 +++++++
 7 files changed, 200 insertions(+), 7 deletions(-)

diff --git 
a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperations.java
 
b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperations.java
index 59ede92f47fa..17bb0435d368 100644
--- 
a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperations.java
+++ 
b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperations.java
@@ -20,6 +20,8 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.time.Duration;
 import java.time.OffsetDateTime;
@@ -644,10 +646,43 @@ public class BlobOperations {
         final Path normalizedDir = new File(fileDir).toPath().normalize();
         final Path normalizedTarget = target.toPath().normalize();
         if (!normalizedTarget.startsWith(normalizedDir)) {
+            throw outsideDirectory(name, fileDir);
+        }
+
+        try {
+            final Path resolvedDir = resolveExistingPathSegments(new 
File(fileDir).toPath());
+            final Path resolvedTarget = 
resolveExistingPathSegments(target.toPath());
+            if (!resolvedTarget.startsWith(resolvedDir)) {
+                throw outsideDirectory(name, fileDir);
+            }
+        } catch (IOException e) {
             throw new IllegalArgumentException(
-                    "Cannot download to file '" + name
-                                               + "' as it resolves outside the 
configured fileDir directory: " + fileDir);
+                    "Cannot verify download path for file '" + name + "' 
within the configured fileDir directory: "
+                                               + fileDir,
+                    e);
         }
         return target;
     }
+
+    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();
+    }
+
+    private static IllegalArgumentException outsideDirectory(String name, 
String fileDir) {
+        return new IllegalArgumentException(
+                "Cannot download to file '" + name
+                                            + "' as it resolves outside the 
configured fileDir directory: " + fileDir);
+    }
 }
diff --git 
a/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperationsTest.java
 
b/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperationsTest.java
index 6c4b23671a29..f8744d961d0f 100644
--- 
a/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperationsTest.java
+++ 
b/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperationsTest.java
@@ -23,6 +23,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.time.OffsetDateTime;
 import java.util.HashMap;
 import java.util.Map;
@@ -45,6 +47,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
@@ -137,6 +140,24 @@ class BlobOperationsTest extends CamelTestSupport {
         assertThrows(IllegalArgumentException.class, () -> 
operations.downloadBlobToFile(exchange));
     }
 
+    @Test
+    void 
testDownloadBlobToFileRejectsSymbolicLinkResolvingOutsideDirectory(@TempDir 
Path parent) throws IOException {
+        Path downloadDir = Files.createDirectory(parent.resolve("downloads"));
+        Path outsideDir = Files.createDirectory(parent.resolve("outside"));
+        Files.createSymbolicLink(downloadDir.resolve("linked"), outsideDir);
+
+        configuration.setFileDir(downloadDir.toString());
+        when(client.getBlobName()).thenReturn("linked/PROOF_PWNED");
+
+        final BlobOperations operations = new BlobOperations(configuration, 
client);
+        final Exchange exchange = new DefaultExchange(context);
+
+        final IllegalArgumentException exception
+                = assertThrows(IllegalArgumentException.class, () -> 
operations.downloadBlobToFile(exchange));
+        assertTrue(exception.getMessage().contains("linked/PROOF_PWNED"));
+        assertTrue(exception.getMessage().contains(downloadDir.toString()));
+    }
+
     @Test
     void testUploadBlockBlob() throws Exception {
         // mocking
diff --git 
a/components/camel-azure/camel-azure-storage-datalake/src/main/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperations.java
 
b/components/camel-azure/camel-azure-storage-datalake/src/main/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperations.java
index 6a2c92a403af..58749f17d44e 100644
--- 
a/components/camel-azure/camel-azure-storage-datalake/src/main/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperations.java
+++ 
b/components/camel-azure/camel-azure-storage-datalake/src/main/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperations.java
@@ -20,6 +20,8 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.time.Duration;
@@ -262,10 +264,43 @@ public class DataLakeFileOperations {
         final Path normalizedDir = new File(fileDir).toPath().normalize();
         final Path normalizedTarget = target.toPath().normalize();
         if (!normalizedTarget.startsWith(normalizedDir)) {
+            throw outsideDirectory(name, fileDir);
+        }
+
+        try {
+            final Path resolvedDir = resolveExistingPathSegments(new 
File(fileDir).toPath());
+            final Path resolvedTarget = 
resolveExistingPathSegments(target.toPath());
+            if (!resolvedTarget.startsWith(resolvedDir)) {
+                throw outsideDirectory(name, fileDir);
+            }
+        } catch (IOException e) {
             throw new IllegalArgumentException(
-                    "Cannot download to file '" + name
-                                               + "' as it resolves outside the 
configured fileDir directory: " + fileDir);
+                    "Cannot verify download path for file '" + name + "' 
within the configured fileDir directory: "
+                                               + fileDir,
+                    e);
         }
         return target;
     }
+
+    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();
+    }
+
+    private static IllegalArgumentException outsideDirectory(String name, 
String fileDir) {
+        return new IllegalArgumentException(
+                "Cannot download to file '" + name
+                                            + "' as it resolves outside the 
configured fileDir directory: " + fileDir);
+    }
 }
diff --git 
a/components/camel-azure/camel-azure-storage-datalake/src/test/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperationTest.java
 
b/components/camel-azure/camel-azure-storage-datalake/src/test/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperationTest.java
index d2db2a7ac0f8..3b8c131548bc 100644
--- 
a/components/camel-azure/camel-azure-storage-datalake/src/test/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperationTest.java
+++ 
b/components/camel-azure/camel-azure-storage-datalake/src/test/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperationTest.java
@@ -22,6 +22,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.time.OffsetDateTime;
 
 import com.azure.core.http.HttpHeaders;
@@ -39,6 +41,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
@@ -107,6 +110,24 @@ public class DataLakeFileOperationTest extends 
CamelTestSupport {
         assertThrows(IllegalArgumentException.class, () -> 
operations.downloadToFile(exchange));
     }
 
+    @Test
+    void 
testDownloadToFileRejectsSymbolicLinkResolvingOutsideDirectory(@TempDir Path 
parent) throws IOException {
+        Path downloadDir = Files.createDirectory(parent.resolve("downloads"));
+        Path outsideDir = Files.createDirectory(parent.resolve("outside"));
+        Files.createSymbolicLink(downloadDir.resolve("linked"), outsideDir);
+
+        configuration.setFileDir(downloadDir.toString());
+        when(client.getFileName()).thenReturn("linked/PROOF_PWNED");
+
+        final DataLakeFileOperations operations = new 
DataLakeFileOperations(configuration, client);
+        final Exchange exchange = new DefaultExchange(context);
+
+        final IllegalArgumentException exception
+                = assertThrows(IllegalArgumentException.class, () -> 
operations.downloadToFile(exchange));
+        assertTrue(exception.getMessage().contains("linked/PROOF_PWNED"));
+        assertTrue(exception.getMessage().contains(downloadDir.toString()));
+    }
+
     @Test
     void testUploadFile() throws Exception {
         final OffsetDateTime time = OffsetDateTime.now();
diff --git 
a/components/camel-google/camel-google-storage/src/main/java/org/apache/camel/component/google/storage/GoogleCloudStorageFileNameHelper.java
 
b/components/camel-google/camel-google-storage/src/main/java/org/apache/camel/component/google/storage/GoogleCloudStorageFileNameHelper.java
index 59ca3a2b6e2e..53165cefa813 100644
--- 
a/components/camel-google/camel-google-storage/src/main/java/org/apache/camel/component/google/storage/GoogleCloudStorageFileNameHelper.java
+++ 
b/components/camel-google/camel-google-storage/src/main/java/org/apache/camel/component/google/storage/GoogleCloudStorageFileNameHelper.java
@@ -17,6 +17,9 @@
 package org.apache.camel.component.google.storage;
 
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
 import java.nio.file.Path;
 
 /**
@@ -47,10 +50,44 @@ final class GoogleCloudStorageFileNameHelper {
         final Path normalizedDir = new 
File(downloadDirectory).toPath().normalize();
         final Path normalizedTarget = new 
File(resolvedPath).toPath().normalize();
         if (!normalizedTarget.startsWith(normalizedDir)) {
+            throw outsideDirectory(objectName, downloadDirectory);
+        }
+
+        try {
+            final Path resolvedDir = resolveExistingPathSegments(new 
File(downloadDirectory).toPath());
+            final Path resolvedTarget = resolveExistingPathSegments(new 
File(resolvedPath).toPath());
+            if (!resolvedTarget.startsWith(resolvedDir)) {
+                throw outsideDirectory(objectName, downloadDirectory);
+            }
+        } catch (IOException e) {
             throw new IllegalArgumentException(
-                    "Cannot download to file '" + objectName
-                                               + "' as it resolves outside the 
configured downloadFileName directory: "
-                                               + downloadDirectory);
+                    "Cannot verify download path for file '" + objectName
+                                               + "' within the configured 
downloadFileName directory: "
+                                               + downloadDirectory,
+                    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();
+    }
+
+    private static IllegalArgumentException outsideDirectory(String 
objectName, String downloadDirectory) {
+        return new IllegalArgumentException(
+                "Cannot download to file '" + objectName
+                                            + "' as it resolves outside the 
configured downloadFileName directory: "
+                                            + downloadDirectory);
     }
 }
diff --git 
a/components/camel-google/camel-google-storage/src/test/java/org/apache/camel/component/google/storage/GoogleCloudStorageFileNameHelperTest.java
 
b/components/camel-google/camel-google-storage/src/test/java/org/apache/camel/component/google/storage/GoogleCloudStorageFileNameHelperTest.java
index d84c9e42468d..167f9263e9df 100644
--- 
a/components/camel-google/camel-google-storage/src/test/java/org/apache/camel/component/google/storage/GoogleCloudStorageFileNameHelperTest.java
+++ 
b/components/camel-google/camel-google-storage/src/test/java/org/apache/camel/component/google/storage/GoogleCloudStorageFileNameHelperTest.java
@@ -16,7 +16,12 @@
  */
 package org.apache.camel.component.google.storage;
 
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static 
org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -80,4 +85,30 @@ class GoogleCloudStorageFileNameHelperTest {
                 .isThrownBy(() -> 
GoogleCloudStorageFileNameHelper.assertWithinDirectory(DIR,
                         DIR + "-evil/file.txt", 
"../gcs-download-evil/file.txt"));
     }
+
+    @Test
+    void symbolicLinkResolvingOutsideDirectoryIsRejected(@TempDir Path parent) 
throws IOException {
+        Path downloadDir = Files.createDirectory(parent.resolve("downloads"));
+        Path outsideDir = Files.createDirectory(parent.resolve("outside"));
+        Path linkedPath = 
Files.createSymbolicLink(downloadDir.resolve("linked"), outsideDir);
+
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> 
GoogleCloudStorageFileNameHelper.assertWithinDirectory(
+                        downloadDir.toString(), 
linkedPath.resolve("file.txt").toString(), "linked/file.txt"))
+                .withMessageContaining("linked/file.txt")
+                .withMessageContaining(downloadDir.toString());
+    }
+
+    @Test
+    void parentSegmentAfterSymbolicLinkIsResolvedByFilesystem(@TempDir Path 
parent) throws IOException {
+        Path downloadDir = Files.createDirectory(parent.resolve("downloads"));
+        Path outsideDir = 
Files.createDirectories(parent.resolve("outside/child"));
+        Path linkedPath = 
Files.createSymbolicLink(downloadDir.resolve("linked"), outsideDir);
+
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> 
GoogleCloudStorageFileNameHelper.assertWithinDirectory(
+                        downloadDir.toString(), 
linkedPath.resolve("../file.txt").toString(), "linked/../file.txt"))
+                .withMessageContaining("linked/../file.txt")
+                .withMessageContaining(downloadDir.toString());
+    }
 }
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
index ea02914342af..bf669caf9230 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
@@ -33,6 +33,19 @@ The same default is now also applied to the `ClientConfig` 
that Camel builds for
 endpoints, when neither a referenced `ClientConfig` nor `hazelcastConfigUri` 
is supplied. Client mode
 previously behaved differently from node mode for an otherwise identical 
endpoint configuration.
 
+=== camel-azure-storage-blob and camel-azure-storage-datalake
+
+Local downloads configured with `fileDir` now resolve existing filesystem path 
segments before checking
+that the destination remains inside the configured directory. Downloads 
through a symbolic link that
+resolves outside `fileDir` are rejected. Valid nested download paths continue 
to work.
+
+=== camel-google-storage
+
+Local downloads configured with a plain `downloadFileName` directory now 
resolve existing filesystem
+path segments before checking that the destination remains inside that 
directory. Downloads through a
+symbolic link that resolves outside the configured directory are rejected. 
Valid object names using `/`
+as a pseudo-directory separator continue to work.
+
 == Upgrading from 4.18.3 to 4.18.4
 
 === camel-core - Multicast EIP honors UseOriginalAggregationStrategy

Reply via email to