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

oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 55fcbaf8bc19 CAMEL-23942: contain Azure Storage Blob/DataLake fileDir 
downloads within the configured directory (#24542)
55fcbaf8bc19 is described below

commit 55fcbaf8bc1936cae90abdea5adecbc43e57340e
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Jul 8 23:05:51 2026 +0200

    CAMEL-23942: contain Azure Storage Blob/DataLake fileDir downloads within 
the configured directory (#24542)
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
 .../azure/common/AzureFileNameHelper.java          | 53 +++++++++++++++
 .../azure/common/AzureFileNameHelperTest.java      | 75 ++++++++++++++++++++++
 .../storage/blob/operations/BlobOperations.java    |  3 +-
 .../blob/operations/BlobOperationsTest.java        | 13 ++++
 .../operations/DataLakeFileOperations.java         |  3 +-
 .../operations/DataLakeFileOperationTest.java      | 14 ++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    | 10 +++
 7 files changed, 169 insertions(+), 2 deletions(-)

diff --git 
a/components/camel-azure/camel-azure-common/src/main/java/org/apache/camel/component/azure/common/AzureFileNameHelper.java
 
b/components/camel-azure/camel-azure-common/src/main/java/org/apache/camel/component/azure/common/AzureFileNameHelper.java
new file mode 100644
index 000000000000..371f7f9faa56
--- /dev/null
+++ 
b/components/camel-azure/camel-azure-common/src/main/java/org/apache/camel/component/azure/common/AzureFileNameHelper.java
@@ -0,0 +1,53 @@
+/*
+ * 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.azure.common;
+
+import java.io.File;
+import java.nio.file.Path;
+
+/**
+ * Utility methods for safely handling local file paths derived from remote 
Azure Storage object names.
+ */
+public final class AzureFileNameHelper {
+
+    private AzureFileNameHelper() {
+    }
+
+    /**
+     * Resolves a remote object {@code name} against the configured local 
{@code fileDir}, ensuring the resulting file
+     * stays within that directory. A remote object name is influenced by 
whoever writes to the storage container and
+     * may contain {@code ../} segments that would otherwise resolve to a 
location outside {@code fileDir}.
+     *
+     * @param  fileDir                  the configured local directory the 
download must stay within
+     * @param  name                     the remote object name used to build 
the local file
+     * @return                          the resolved local {@link File}, 
guaranteed to be within {@code fileDir}
+     * @throws IllegalArgumentException if the resolved file would be located 
outside {@code fileDir}
+     */
+    public 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-common/src/test/java/org/apache/camel/component/azure/common/AzureFileNameHelperTest.java
 
b/components/camel-azure/camel-azure-common/src/test/java/org/apache/camel/component/azure/common/AzureFileNameHelperTest.java
new file mode 100644
index 000000000000..4668800d6e70
--- /dev/null
+++ 
b/components/camel-azure/camel-azure-common/src/test/java/org/apache/camel/component/azure/common/AzureFileNameHelperTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.azure.common;
+
+import java.io.File;
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class AzureFileNameHelperTest {
+
+    @Test
+    void shouldResolveSimpleNameWithinDirectory(@TempDir Path dir) {
+        final File result = 
AzureFileNameHelper.resolveWithinDirectory(dir.toString(), "file.txt");
+
+        assertEquals(new File(dir.toFile(), "file.txt"), result);
+        assertTrue(result.toPath().normalize().startsWith(dir));
+    }
+
+    @Test
+    void shouldResolveNestedNameWithinDirectory(@TempDir Path dir) {
+        final File result = 
AzureFileNameHelper.resolveWithinDirectory(dir.toString(), "sub/dir/file.txt");
+
+        assertTrue(result.toPath().normalize().startsWith(dir));
+    }
+
+    @Test
+    void shouldAllowInternalDotDotThatStaysWithinDirectory(@TempDir Path dir) {
+        // "sub/../file.txt" normalizes back to "file.txt" inside the 
directory, so it must be accepted
+        final File result = 
AzureFileNameHelper.resolveWithinDirectory(dir.toString(), "sub/../file.txt");
+
+        assertTrue(result.toPath().normalize().startsWith(dir));
+    }
+
+    @Test
+    void shouldRejectParentTraversal(@TempDir Path dir) {
+        assertThrows(IllegalArgumentException.class,
+                () -> 
AzureFileNameHelper.resolveWithinDirectory(dir.toString(), 
"../escaped/PROOF_PWNED"));
+    }
+
+    @Test
+    void shouldRejectDeepParentTraversal(@TempDir Path dir) {
+        assertThrows(IllegalArgumentException.class,
+                () -> 
AzureFileNameHelper.resolveWithinDirectory(dir.toString(), 
"../../../../etc/cron.d/evil"));
+    }
+
+    @Test
+    void shouldRejectSiblingDirectoryWithSharedPrefix(@TempDir Path parent) {
+        // fileDir=<parent>/work; the name escapes to the sibling 
<parent>/workspace which shares the "work" prefix.
+        // A bare string-prefix check would wrongly accept this; a 
path-segment boundary check rejects it.
+        final String fileDir = parent.resolve("work").toString();
+
+        assertThrows(IllegalArgumentException.class,
+                () -> AzureFileNameHelper.resolveWithinDirectory(fileDir, 
"../workspace/secret"));
+    }
+}
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 00c5dff10065..bf0ff88dfb95 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
@@ -60,6 +60,7 @@ import com.azure.storage.blob.specialized.BlobLeaseClient;
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
 import org.apache.camel.WrappedFile;
+import org.apache.camel.component.azure.common.AzureFileNameHelper;
 import org.apache.camel.component.azure.storage.blob.BlobBlock;
 import org.apache.camel.component.azure.storage.blob.BlobCommonRequestOptions;
 import org.apache.camel.component.azure.storage.blob.BlobConfiguration;
@@ -153,7 +154,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 = 
AzureFileNameHelper.resolveWithinDirectory(fileDir, client.getBlobName());
         final BlobCommonRequestOptions commonRequestOptions = 
getCommonRequestOptions(exchange);
         final BlobRange blobRange = configurationProxy.getBlobRange(exchange);
         final ParallelTransferOptions parallelTransferOptions = 
configurationProxy.getParallelTransferOptions(exchange);
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 9183c0b9bf77..9c35ec259f94 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
@@ -134,6 +134,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..0307ad8469a9 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
@@ -44,6 +44,7 @@ import 
com.azure.storage.file.datalake.sas.DataLakeServiceSasSignatureValues;
 import com.azure.storage.file.datalake.sas.PathSasPermission;
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
+import org.apache.camel.component.azure.common.AzureFileNameHelper;
 import org.apache.camel.component.azure.storage.datalake.DataLakeConfiguration;
 import 
org.apache.camel.component.azure.storage.datalake.DataLakeConfigurationOptionsProxy;
 import 
org.apache.camel.component.azure.storage.datalake.DataLakeExchangeHeaders;
@@ -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 = 
AzureFileNameHelper.resolveWithinDirectory(fileDir, 
fileClientWrapper.getFileName());
         final FileCommonRequestOptions commonRequestOptions = 
getCommonRequestOptions(exchange);
         final FileRange fileRange = configurationProxy.getFileRange(exchange);
         final ParallelTransferOptions parallelTransferOptions = 
configurationProxy.getParallelTransferOptions(exchange);
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 82c0e2bb976c..7a8d25f35544 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_22.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index d6a7b9e751b7..0f3174063d3b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -204,3 +204,13 @@ suitable for scripting, use the `--json` option.
 The `camel infra list` table now sizes its `DESCRIPTION` column to the 
terminal width, and truncates
 the `IMPLEMENTATION` and `SERVICE_DATA` columns with an ellipsis instead of 
letting the raw service
 data overflow the terminal. The complete, structured service data remains 
available via `--json`.
+
+=== 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 (see the `localWorkDirectory` note in the 4.21 
upgrade guide).
+
+Ordinary object names are unaffected. A name that resolves outside `fileDir` 
is now rejected with an
+`IllegalArgumentException`.

Reply via email to