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

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


The following commit(s) were added to refs/heads/main by this push:
     new 78df4c14b98 NIFI-16110 Improved MiNiFi C2 Asset File Name Validation 
(#11427)
78df4c14b98 is described below

commit 78df4c14b98114dfc7127061684684fb7d186182
Author: David Handermann <[email protected]>
AuthorDate: Wed Jul 15 03:28:07 2026 -0500

    NIFI-16110 Improved MiNiFi C2 Asset File Name Validation (#11427)
    
    - Aligned Asset File Name validation for update and synchronization 
strategies
---
 .../c2/command/UpdateAssetCommandHelper.java       | 61 +++++++++++++------
 .../syncresource/DefaultSyncResourceStrategy.java  |  4 +-
 .../c2/command/UpdateAssetCommandHelperTest.java   | 69 +++++++++++++---------
 3 files changed, 85 insertions(+), 49 deletions(-)

diff --git 
a/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-framework-core/src/main/java/org/apache/nifi/minifi/c2/command/UpdateAssetCommandHelper.java
 
b/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-framework-core/src/main/java/org/apache/nifi/minifi/c2/command/UpdateAssetCommandHelper.java
index 166168bca86..102f55077f7 100644
--- 
a/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-framework-core/src/main/java/org/apache/nifi/minifi/c2/command/UpdateAssetCommandHelper.java
+++ 
b/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-framework-core/src/main/java/org/apache/nifi/minifi/c2/command/UpdateAssetCommandHelper.java
@@ -25,9 +25,15 @@ import java.io.UncheckedIOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.regex.Pattern.compile;
 
 public class UpdateAssetCommandHelper {
 
+    public static final Pattern ALLOWED_RESOURCE_PATH_PATTERN = 
compile("^(?:[^~<>:|\"?*./\\\\]+(?:[/\\\\][^~<>:|\"?*./\\\\]+)*)?$");
+
     private static final Logger LOG = 
LoggerFactory.getLogger(UpdateAssetCommandHelper.class);
 
     private final String assetDirectory;
@@ -45,26 +51,47 @@ public class UpdateAssetCommandHelper {
         }
     }
 
-    public boolean assetUpdatePrecondition(String assetFileName, Boolean 
forceDownload) {
-        Path assetPath = Paths.get(assetDirectory, assetFileName);
-        if (Files.exists(assetPath) && !forceDownload) {
-            LOG.info("Asset file already exists on path {}. Asset won't be 
downloaded", assetPath);
-            return false;
+    public boolean assetUpdatePrecondition(final String assetFileName, final 
Boolean forceDownload) {
+        final boolean downloadEnabled;
+
+        final Matcher assetFileNameMatcher = 
ALLOWED_RESOURCE_PATH_PATTERN.matcher(assetFileName);
+        if (assetFileNameMatcher.matches()) {
+            final Path assetPath = Paths.get(assetDirectory, assetFileName);
+            if (Files.exists(assetPath) && !forceDownload) {
+                LOG.info("Asset File found at [{}] Download disabled", 
assetPath);
+                downloadEnabled = false;
+            } else {
+                LOG.info("Asset File not found at [{}] or Force Download 
enabled", assetPath);
+                downloadEnabled = true;
+            }
+        } else {
+            LOG.warn("Asset File Name [{}] not allowed for downloading", 
assetFileName);
+            downloadEnabled = false;
         }
-        LOG.info("Asset file does not exist or force download is on. Asset 
will be downloaded to {}", assetPath);
-        return true;
+
+        return downloadEnabled;
     }
 
-    public boolean assetPersistFunction(String assetFileName, byte[] 
assetBinary) {
-        Path assetPath = Paths.get(assetDirectory, assetFileName);
-        try {
-            Files.deleteIfExists(assetPath);
-            Files.write(assetPath, assetBinary);
-            LOG.info("Asset was persisted to {}, {} bytes were written", 
assetPath, assetBinary.length);
-            return true;
-        } catch (IOException e) {
-            LOG.error("Persisting asset failed. File creation was not 
successful targeting {}", assetPath, e);
-            return false;
+    public boolean assetPersistFunction(final String assetFileName, final 
byte[] assetBinary) {
+        boolean persisted;
+
+        final Matcher assetFileNameMatcher = 
ALLOWED_RESOURCE_PATH_PATTERN.matcher(assetFileName);
+        if (assetFileNameMatcher.matches()) {
+            final Path assetPath = Paths.get(assetDirectory, assetFileName);
+            try {
+                Files.deleteIfExists(assetPath);
+                Files.write(assetPath, assetBinary);
+                LOG.info("Asset was persisted to {}, {} bytes were written", 
assetPath, assetBinary.length);
+                persisted = true;
+            } catch (final IOException e) {
+                LOG.error("Persisting asset failed. File creation was not 
successful targeting {}", assetPath, e);
+                persisted = false;
+            }
+        } else {
+            LOG.warn("Asset File Name [{}] not allowed for writing", 
assetFileName);
+            persisted = false;
         }
+
+        return persisted;
     }
 }
diff --git 
a/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-framework-core/src/main/java/org/apache/nifi/minifi/c2/command/syncresource/DefaultSyncResourceStrategy.java
 
b/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-framework-core/src/main/java/org/apache/nifi/minifi/c2/command/syncresource/DefaultSyncResourceStrategy.java
index 228e8656d6f..f98012d62b6 100644
--- 
a/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-framework-core/src/main/java/org/apache/nifi/minifi/c2/command/syncresource/DefaultSyncResourceStrategy.java
+++ 
b/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-framework-core/src/main/java/org/apache/nifi/minifi/c2/command/syncresource/DefaultSyncResourceStrategy.java
@@ -33,7 +33,6 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.function.Function;
-import java.util.regex.Pattern;
 
 import static java.nio.file.Files.copy;
 import static java.nio.file.Files.createTempFile;
@@ -42,17 +41,16 @@ import static java.util.Map.entry;
 import static java.util.Optional.empty;
 import static java.util.UUID.randomUUID;
 import static java.util.function.Predicate.not;
-import static java.util.regex.Pattern.compile;
 import static 
org.apache.nifi.c2.protocol.api.C2OperationState.OperationState.FULLY_APPLIED;
 import static 
org.apache.nifi.c2.protocol.api.C2OperationState.OperationState.NOT_APPLIED;
 import static 
org.apache.nifi.c2.protocol.api.C2OperationState.OperationState.NO_OPERATION;
 import static 
org.apache.nifi.c2.protocol.api.C2OperationState.OperationState.PARTIALLY_APPLIED;
 import static org.apache.nifi.c2.protocol.api.ResourceType.ASSET;
+import static 
org.apache.nifi.minifi.c2.command.UpdateAssetCommandHelper.ALLOWED_RESOURCE_PATH_PATTERN;
 
 public class DefaultSyncResourceStrategy implements SyncResourceStrategy {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(DefaultSyncResourceStrategy.class);
-    private static final Pattern ALLOWED_RESOURCE_PATH_PATTERN = 
compile("^(?:[^~<>:\\|\\\"\\?\\*\\.\\/\\\\]+(?:[/\\\\][^~<>:\\|\\\"\\?\\*\\.\\/\\\\]+)*)?$");
 
     private static final Set<Entry<OperationState, OperationState>> 
SUCCESS_RESULT_PAIRS = Set.of(
         entry(NO_OPERATION, NO_OPERATION),
diff --git 
a/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-framework-core/src/test/java/org/apache/nifi/minifi/c2/command/UpdateAssetCommandHelperTest.java
 
b/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-framework-core/src/test/java/org/apache/nifi/minifi/c2/command/UpdateAssetCommandHelperTest.java
index 33f03f838e3..663b9d87374 100644
--- 
a/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-framework-core/src/test/java/org/apache/nifi/minifi/c2/command/UpdateAssetCommandHelperTest.java
+++ 
b/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-framework-core/src/test/java/org/apache/nifi/minifi/c2/command/UpdateAssetCommandHelperTest.java
@@ -20,31 +20,43 @@ package org.apache.nifi.minifi.c2.command;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.FieldSource;
 
 import java.io.File;
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
 import static java.lang.Boolean.FALSE;
 import static java.lang.Boolean.TRUE;
 import static java.nio.charset.Charset.defaultCharset;
-import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.nio.file.Files.exists;
 import static java.nio.file.Files.isDirectory;
 import static java.nio.file.Files.readAllLines;
-import static java.nio.file.Files.write;
 import static java.util.Collections.singletonList;
 import static org.apache.commons.lang3.StringUtils.EMPTY;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertIterableEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-public class UpdateAssetCommandHelperTest {
+class UpdateAssetCommandHelperTest {
+
+    private static final String[] INVALID_ASSET_FILE_NAMES = new String[]{
+            "~",
+            "./",
+            "../",
+            "./../",
+            "../sibling-directory",
+            "/tmp"
+    };
 
     private static final String ASSET_DIRECTORY = "asset_directory";
-    private static final String ASSET_FILE = "asset.file";
+    private static final String ASSET_FILE = "asset-file";
+    private static final byte[] CONTENT = 
String.class.getSimpleName().getBytes(StandardCharsets.UTF_8);
 
     @TempDir
     private File tempDir;
@@ -53,89 +65,88 @@ public class UpdateAssetCommandHelperTest {
     private UpdateAssetCommandHelper updateAssetCommandHelper;
 
     @BeforeEach
-    public void setUp() {
+    void setUp() {
         assetDirectory = Paths.get(tempDir.getAbsolutePath(), ASSET_DIRECTORY);
         updateAssetCommandHelper = new 
UpdateAssetCommandHelper(assetDirectory.toString());
     }
 
     @Test
-    public void testAssetDirectoryShouldBeCreated() {
-        // given + when
+    void testAssetDirectoryShouldBeCreated() {
         updateAssetCommandHelper.createAssetDirectory();
 
-        // then
         assertTrue(exists(assetDirectory));
         assertTrue(isDirectory(assetDirectory));
     }
 
     @Test
-    public void testAssetFileDoesNotExist() {
-        // given
+    void testAssetFileDoesNotExist() {
         updateAssetCommandHelper.createAssetDirectory();
 
-        // when
         boolean result = 
updateAssetCommandHelper.assetUpdatePrecondition(ASSET_FILE, FALSE);
 
-        // then
         assertTrue(result);
     }
 
     @Test
-    public void testAssetFileExistsAndForceDownloadOff() {
-        // given
+    void testAssetFileExistsAndForceDownloadOff() {
         updateAssetCommandHelper.createAssetDirectory();
         touchAssetFile();
 
-        // when
         boolean result = 
updateAssetCommandHelper.assetUpdatePrecondition(ASSET_FILE, FALSE);
 
-        // then
         assertFalse(result);
     }
 
     @Test
-    public void testAssetFileExistsAndForceDownloadOn() {
-        // given
+    void testAssetFileExistsAndForceDownloadOn() {
         updateAssetCommandHelper.createAssetDirectory();
         touchAssetFile();
 
-        // when
         boolean result = 
updateAssetCommandHelper.assetUpdatePrecondition(ASSET_FILE, TRUE);
 
-        // then
         assertTrue(result);
     }
 
     @Test
-    public void testAssetPersistedCorrectly() throws IOException {
-        // given
+    void testAssetPersistedCorrectly() throws IOException {
         updateAssetCommandHelper.createAssetDirectory();
         String testAssetContent = "test file content";
 
-        // when
         boolean result = 
updateAssetCommandHelper.assetPersistFunction(ASSET_FILE, 
testAssetContent.getBytes(defaultCharset()));
 
-        // then
         assertTrue(result);
         assertIterableEquals(singletonList(testAssetContent), 
readAllLines(assetDirectory.resolve(ASSET_FILE), defaultCharset()));
     }
 
     @Test
-    public void testAssetDirectoryDoesNotExistWhenPersistingAsset() {
-        // given
+    void testAssetDirectoryDoesNotExistWhenPersistingAsset() {
         String testAssetContent = "test file content";
 
-        // when
         boolean result = 
updateAssetCommandHelper.assetPersistFunction(ASSET_FILE, 
testAssetContent.getBytes(defaultCharset()));
 
-        // then
         assertFalse(result);
         assertFalse(exists(assetDirectory.resolve(ASSET_FILE)));
     }
 
+    @ParameterizedTest
+    @FieldSource("INVALID_ASSET_FILE_NAMES")
+    void testAssetUpdatePreconditionDirectoriesDisallowed(final String 
assetName) {
+        final boolean completed = 
updateAssetCommandHelper.assetUpdatePrecondition(assetName, true);
+
+        assertFalse(completed);
+    }
+
+    @ParameterizedTest
+    @FieldSource("INVALID_ASSET_FILE_NAMES")
+    void testAssetPersistFunctionDirectoriesDisallowed(final String assetName) 
{
+        final boolean completed = 
updateAssetCommandHelper.assetPersistFunction(assetName, CONTENT);
+
+        assertFalse(completed);
+    }
+
     private void touchAssetFile() {
         try {
-            write(Paths.get(assetDirectory.toString(), ASSET_FILE), 
EMPTY.getBytes(UTF_8));
+            Files.writeString(Paths.get(assetDirectory.toString(), 
ASSET_FILE), EMPTY);
         } catch (IOException e) {
             throw new UncheckedIOException("Failed to touch file", e);
         }

Reply via email to