You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ac...@apache.org on 2026/07/10 13:19:24 UTC

(camel) branch camel-4.18.x updated: [backport camel-4.18.x] CAMEL-23942: contain Azure Storage Blob/DataLake fileDir downloads within the configured directory (#24581)

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

oscerd 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 007929bb732a [backport camel-4.18.x] CAMEL-23942: contain Azure Storage Blob/DataLake fileDir downloads within the configured directory (#24581)
007929bb732a is described below

commit 007929bb732a8c5e3e49d4d3c6c8adc2aab13340
Author: Andrea Cosentino <an...@gmail.com>
AuthorDate: Fri Jul 10 15:19:14 2026 +0200

    [backport camel-4.18.x] CAMEL-23942: contain Azure Storage Blob/DataLake fileDir downloads within the configured directory (#24581)
    
    Signed-off-by: Andrea Cosentino <an...@gmail.com>
    Co-authored-by: Claude Opus 4.8 (1M context) <no...@anthropic.com>
---
 .../azure/storage/blob/operations/BlobOperations.java   | 17 ++++++++++++++++-
 .../storage/blob/operations/BlobOperationsTest.java     | 13 +++++++++++++
 .../datalake/operations/DataLakeFileOperations.java     | 17 ++++++++++++++++-
 .../datalake/operations/DataLakeFileOperationTest.java  | 14 ++++++++++++++
 .../modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc | 10 ++++++++++
 5 files changed, 69 insertions(+), 2 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 c577d3604715..59ede92f47fa 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,7 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.file.Path;
 import java.time.Duration;
 import java.time.OffsetDateTime;
 import java.util.Collections;
@@ -145,7 +146,7 @@ public class BlobOperations {
             throw new IllegalArgumentException("In order to download a blob, you will need to specify the fileDir in the URI");
         }
 
-        final File fileToDownload = new File(fileDir, client.getBlobName());
+        final File fileToDownload = resolveWithinDirectory(fileDir, client.getBlobName());
         final BlobCommonRequestOptions commonRequestOptions = getCommonRequestOptions(exchange);
         final BlobRange blobRange = configurationProxy.getBlobRange(exchange);
         final ParallelTransferOptions parallelTransferOptions = configurationProxy.getParallelTransferOptions(exchange);
@@ -635,4 +636,18 @@ public class BlobOperations {
             leaseClient.releaseLease();
         }
     }
+
+    private static File resolveWithinDirectory(String fileDir, String name) {
+        final File target = new File(fileDir, name);
+        // normalize lexically (removes ./ and ../ segments) and compare on path-segment boundaries so a sibling
+        // directory whose name merely extends fileDir is not considered contained
+        final Path normalizedDir = new File(fileDir).toPath().normalize();
+        final Path normalizedTarget = target.toPath().normalize();
+        if (!normalizedTarget.startsWith(normalizedDir)) {
+            throw new IllegalArgumentException(
+                    "Cannot download to file '" + name
+                                               + "' as it resolves outside the configured fileDir directory: " + fileDir);
+        }
+        return target;
+    }
 }
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 3e7755113dfe..6c4b23671a29 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
@@ -124,6 +124,19 @@ class BlobOperationsTest extends CamelTestSupport {
         assertEquals("tag1", response3.getHeaders().get(BlobConstants.E_TAG));
     }
 
+    @Test
+    void testDownloadBlobToFileRejectsPathTraversalName() {
+        // a blob name is influenced by whoever writes to the container and may contain ../ segments;
+        // the download must stay within the configured fileDir
+        configuration.setFileDir("/tmp/camel-azure-blob-download");
+        when(client.getBlobName()).thenReturn("../escaped/PROOF_PWNED");
+
+        final BlobOperations operations = new BlobOperations(configuration, client);
+        final Exchange exchange = new DefaultExchange(context);
+
+        assertThrows(IllegalArgumentException.class, () -> operations.downloadBlobToFile(exchange));
+    }
+
     @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 72ab8856a898..6a2c92a403af 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
@@ -21,6 +21,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.file.OpenOption;
+import java.nio.file.Path;
 import java.time.Duration;
 import java.time.OffsetDateTime;
 import java.util.Map;
@@ -106,7 +107,7 @@ public class DataLakeFileOperations {
             throw new IllegalArgumentException("to download a file, you need to specify the fileDir in the URI");
         }
         final DataLakeFileClientWrapper fileClientWrapper = getFileClientWrapper(exchange);
-        final File recieverFile = new File(fileDir, fileClientWrapper.getFileName());
+        final File recieverFile = resolveWithinDirectory(fileDir, fileClientWrapper.getFileName());
         final FileCommonRequestOptions commonRequestOptions = getCommonRequestOptions(exchange);
         final FileRange fileRange = configurationProxy.getFileRange(exchange);
         final ParallelTransferOptions parallelTransferOptions = configurationProxy.getParallelTransferOptions(exchange);
@@ -253,4 +254,18 @@ public class DataLakeFileOperations {
         final DataLakeFileClient fileClient = configurationProxy.getFileClient(exchange);
         return null == fileClient ? client : new DataLakeFileClientWrapper(fileClient);
     }
+
+    private static File resolveWithinDirectory(String fileDir, String name) {
+        final File target = new File(fileDir, name);
+        // normalize lexically (removes ./ and ../ segments) and compare on path-segment boundaries so a sibling
+        // directory whose name merely extends fileDir is not considered contained
+        final Path normalizedDir = new File(fileDir).toPath().normalize();
+        final Path normalizedTarget = target.toPath().normalize();
+        if (!normalizedTarget.startsWith(normalizedDir)) {
+            throw new IllegalArgumentException(
+                    "Cannot download to file '" + name
+                                               + "' as it resolves outside the configured fileDir directory: " + fileDir);
+        }
+        return target;
+    }
 }
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 0c3380d43b7f..d2db2a7ac0f8 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
@@ -44,6 +44,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -93,6 +94,19 @@ public class DataLakeFileOperationTest extends CamelTestSupport {
         assertNotNull(secondResponse.getHeaders());
     }
 
+    @Test
+    void testDownloadToFileRejectsPathTraversalName() {
+        // a file name is influenced by whoever writes to the file system and may contain ../ segments;
+        // the download must stay within the configured fileDir
+        configuration.setFileDir("/tmp/camel-azure-datalake-download");
+        when(client.getFileName()).thenReturn("../escaped/PROOF_PWNED");
+
+        final DataLakeFileOperations operations = new DataLakeFileOperations(configuration, client);
+        final Exchange exchange = new DefaultExchange(context);
+
+        assertThrows(IllegalArgumentException.class, () -> operations.downloadToFile(exchange));
+    }
+
     @Test
     void testUploadFile() throws Exception {
         final OffsetDateTime time = OffsetDateTime.now();
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 0358aaeba807..88002166e0b9 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
@@ -1488,3 +1488,13 @@ non-`Camel`-prefixed application headers and map them to the corresponding
 the `salesforce:` `to`. As defence-in-depth, strip inbound Camel-internal headers
 arriving from untrusted producers with `removeHeaders("CamelSalesforce*")` (or the
 broader `removeHeaders("Camel*")`) before the producer.
+
+=== camel-azure-storage-blob / camel-azure-storage-datalake - download contained within fileDir
+
+When `fileDir` is configured, the Azure Storage Blob and DataLake consumers now ensure the downloaded
+local file stays within the configured directory, so a remote object name containing `../` sequences
+can no longer resolve to a path outside it. This is consistent with the containment already performed
+by the file-based consumers.
+
+Ordinary object names are unaffected. A name that resolves outside `fileDir` is now rejected with an
+`IllegalArgumentException`.