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`.