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

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-configuration.git


The following commit(s) were added to refs/heads/master by this push:
     new b51f6bf26 Detect and avoid processing cycles in YAML input 
(YAMLConfiguration). (#634)
b51f6bf26 is described below

commit b51f6bf26e774f3416fdf782a5e1edf33f32ba82
Author: Gary Gregory <[email protected]>
AuthorDate: Mon May 11 12:24:30 2026 -0400

    Detect and avoid processing cycles in YAML input (YAMLConfiguration). (#634)
---
 .../AbstractYAMLBasedConfiguration.java            | 48 ++++++++++++++--------
 .../commons/configuration2/YAMLConfiguration.java  | 20 +++------
 .../commons/configuration2/io/FileHandler.java     |  3 --
 .../configuration2/TestYAMLConfiguration.java      | 16 +++++++-
 .../apache/commons/configuration2/yaml/cycle.yaml  | 17 ++++++++
 src/test/resources/test.yaml                       |  3 +-
 6 files changed, 69 insertions(+), 38 deletions(-)

diff --git 
a/src/main/java/org/apache/commons/configuration2/AbstractYAMLBasedConfiguration.java
 
b/src/main/java/org/apache/commons/configuration2/AbstractYAMLBasedConfiguration.java
index 1e730c3f8..a1573ec1f 100644
--- 
a/src/main/java/org/apache/commons/configuration2/AbstractYAMLBasedConfiguration.java
+++ 
b/src/main/java/org/apache/commons/configuration2/AbstractYAMLBasedConfiguration.java
@@ -21,13 +21,16 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 import org.apache.commons.configuration2.ex.ConfigurationException;
 import org.apache.commons.configuration2.io.ConfigurationLogger;
 import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.commons.lang3.StringUtils;
 
 /**
  * <p>
@@ -69,44 +72,51 @@ public class AbstractYAMLBasedConfiguration extends 
BaseHierarchicalConfiguratio
     }
 
     /**
-     * Creates a part of the hierarchical nodes structure of the resulting 
configuration. The passed in element is converted
-     * into one or multiple configuration nodes. (If list structures are 
involved, multiple nodes are returned.)
+     * Creates a part of the hierarchical nodes structure of the resulting 
configuration. The passed in element is converted into one or multiple 
configuration
+     * nodes. (If list structures are involved, multiple nodes are returned.)
      *
-     * @param key the key of the new node(s)
-     * @param elem the element to be processed
+     * @param key     the key of the new node(s).
+     * @param elem    the element to be processed.
+     * @param visited the set of visited objects.
      * @return a list with configuration nodes representing the element
      */
-    private static List<ImmutableNode> constructHierarchy(final String key, 
final Object elem) {
+    private static List<ImmutableNode> constructHierarchy(final String key, 
final Object elem, final Set<Object> visited) {
         if (elem instanceof Map) {
-            return parseMap((Map<String, Object>) elem, key);
+            return isVisisted(elem, visited) ? Collections.emptyList() : 
parseMap((Map<String, Object>) elem, key, visited);
         }
         if (elem instanceof Collection) {
-            return parseCollection((Collection<Object>) elem, key);
+            return isVisisted(elem, visited) ? Collections.emptyList() : 
parseCollection((Collection<Object>) elem, key, visited);
         }
         return Collections.singletonList(new 
ImmutableNode.Builder().name(key).value(elem).create());
     }
 
+    private static boolean isVisisted(final Object elem, final Set<Object> 
visited) {
+        return !visited.add(System.identityHashCode(elem));
+    }
+
     /**
      * Parses a collection structure. The elements of the collection are 
processed recursively.
      *
-     * @param col the collection to be processed
-     * @param key the key under which this collection is to be stored
-     * @return a node representing this collection
+     * @param col     the collection to be processed.
+     * @param key     the key under which this collection is to be stored.
+     * @param visited the set of visited objects.
+     * @return a node representing this collection.
      */
-    private static List<ImmutableNode> parseCollection(final 
Collection<Object> col, final String key) {
-        return col.stream().flatMap(elem -> constructHierarchy(key, 
elem).stream()).collect(Collectors.toList());
+    private static List<ImmutableNode> parseCollection(final 
Collection<Object> col, final String key, final Set<Object> visited) {
+        return col.stream().flatMap(elem -> constructHierarchy(key, elem, 
visited).stream()).collect(Collectors.toList());
     }
 
     /**
      * Parses a map structure. The single keys of the map are processed 
recursively.
      *
-     * @param map the map to be processed
-     * @param key the key under which this map is to be stored
+     * @param map     the map to be processed.
+     * @param key     the key under which this map is to be stored.
+     * @param visited the set of visited objects.
      * @return a node representing this map
      */
-    private static List<ImmutableNode> parseMap(final Map<String, Object> map, 
final String key) {
+    private static List<ImmutableNode> parseMap(final Map<String, Object> map, 
final String key, final Set<Object> visited) {
         final ImmutableNode.Builder subtree = new 
ImmutableNode.Builder().name(key);
-        map.forEach((k, v) -> constructHierarchy(k, 
v).forEach(subtree::addChild));
+        map.forEach((k, v) -> constructHierarchy(k, v, 
visited).forEach(subtree::addChild));
         return Collections.singletonList(subtree.create());
     }
 
@@ -159,7 +169,9 @@ public class AbstractYAMLBasedConfiguration extends 
BaseHierarchicalConfiguratio
      * @param map the map to be processed
      */
     protected void load(final Map<String, Object> map) {
-        final List<ImmutableNode> roots = constructHierarchy("", map);
-        getNodeModel().setRootNode(roots.get(0));
+        final List<ImmutableNode> roots = 
constructHierarchy(StringUtils.EMPTY, map, new HashSet<>());
+        if (!roots.isEmpty()) {
+            getNodeModel().setRootNode(roots.get(0));
+        }
     }
 }
diff --git 
a/src/main/java/org/apache/commons/configuration2/YAMLConfiguration.java 
b/src/main/java/org/apache/commons/configuration2/YAMLConfiguration.java
index 72125edbc..6ca91be29 100644
--- a/src/main/java/org/apache/commons/configuration2/YAMLConfiguration.java
+++ b/src/main/java/org/apache/commons/configuration2/YAMLConfiguration.java
@@ -21,7 +21,6 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.Reader;
 import java.io.Writer;
-import java.util.Map;
 
 import org.apache.commons.configuration2.ex.ConfigurationException;
 import org.apache.commons.configuration2.io.InputStreamSupport;
@@ -68,8 +67,7 @@ public class YAMLConfiguration extends 
AbstractYAMLBasedConfiguration implements
 
     public void dump(final Writer out, final DumperOptions options)
             throws ConfigurationException, IOException {
-        final Yaml yaml = new Yaml(options);
-        yaml.dump(constructMap(getNodeModel().getNodeHandler().getRootNode()), 
out);
+        new 
Yaml(options).dump(constructMap(getNodeModel().getNodeHandler().getRootNode()), 
out);
     }
 
     /**
@@ -81,9 +79,7 @@ public class YAMLConfiguration extends 
AbstractYAMLBasedConfiguration implements
     @Override
     public void read(final InputStream in) throws ConfigurationException {
         try {
-            final Yaml yaml = createYamlForReading(new LoaderOptions());
-            final Map<String, Object> map = yaml.load(in);
-            load(map);
+            load(createYamlForReading(new LoaderOptions()).load(in));
         } catch (final Exception e) {
             rethrowException(e);
         }
@@ -91,9 +87,7 @@ public class YAMLConfiguration extends 
AbstractYAMLBasedConfiguration implements
 
     public void read(final InputStream in, final LoaderOptions options) throws 
ConfigurationException {
         try {
-            final Yaml yaml = createYamlForReading(options);
-            final Map<String, Object> map = yaml.load(in);
-            load(map);
+            load(createYamlForReading(options).load(in));
         } catch (final Exception e) {
             rethrowException(e);
         }
@@ -102,9 +96,7 @@ public class YAMLConfiguration extends 
AbstractYAMLBasedConfiguration implements
     @Override
     public void read(final Reader in) throws ConfigurationException {
         try {
-            final Yaml yaml = createYamlForReading(new LoaderOptions());
-            final Map<String, Object> map = yaml.load(in);
-            load(map);
+            load(createYamlForReading(new LoaderOptions()).load(in));
         } catch (final Exception e) {
             rethrowException(e);
         }
@@ -112,9 +104,7 @@ public class YAMLConfiguration extends 
AbstractYAMLBasedConfiguration implements
 
     public void read(final Reader in, final LoaderOptions options) throws 
ConfigurationException {
         try {
-            final Yaml yaml = createYamlForReading(options);
-            final Map<String, Object> map = yaml.load(in);
-            load(map);
+            load(createYamlForReading(options).load(in));
         } catch (final Exception e) {
             rethrowException(e);
         }
diff --git 
a/src/main/java/org/apache/commons/configuration2/io/FileHandler.java 
b/src/main/java/org/apache/commons/configuration2/io/FileHandler.java
index 0f28f36b6..6042f546b 100644
--- a/src/main/java/org/apache/commons/configuration2/io/FileHandler.java
+++ b/src/main/java/org/apache/commons/configuration2/io/FileHandler.java
@@ -586,7 +586,6 @@ public class FileHandler {
         } catch (final MalformedURLException e1) {
             throw new ConfigurationException("Cannot create URL from file %s", 
file);
         }
-
         load(url);
     }
 
@@ -687,7 +686,6 @@ public class FileHandler {
      */
     private void load(final URL url, final FileLocator locator) throws 
ConfigurationException {
         InputStream in = null;
-
         try {
             final FileSystem fileSystem = 
FileLocatorUtils.getFileSystem(locator);
             final URLConnectionOptions urlConnectionOptions = 
locator.getURLConnectionOptions();
@@ -732,7 +730,6 @@ public class FileHandler {
         syncSupport.lock(LockMode.WRITE);
         try {
             injectFileLocator(url);
-
             if (getContent() instanceof InputStreamSupport) {
                 loadFromStreamDirectly(in);
             } else {
diff --git 
a/src/test/java/org/apache/commons/configuration2/TestYAMLConfiguration.java 
b/src/test/java/org/apache/commons/configuration2/TestYAMLConfiguration.java
index 9ba7c319d..c4f2521c5 100644
--- a/src/test/java/org/apache/commons/configuration2/TestYAMLConfiguration.java
+++ b/src/test/java/org/apache/commons/configuration2/TestYAMLConfiguration.java
@@ -34,6 +34,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.io.FileHandler;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -68,9 +69,22 @@ public class TestYAMLConfiguration {
         assertEquals("bar", yamlConfiguration.getString("foo"));
     }
 
+    @Test
+    void testCycle() throws ConfigurationException {
+        final YAMLConfiguration configuration = new YAMLConfiguration();
+        final FileHandler handler = new FileHandler(configuration);
+        handler.load(new 
File("src/test/resources/org/apache/commons/configuration2/yaml/cycle.yaml"));
+    }
+
     @Test
     void testDoubleStringValues() {
-        final Object property = yamlConfiguration.getProperty("key5.example");
+        final Object property = yamlConfiguration.getProperty("key5.example2");
+        assertEquals(Arrays.asList("a", "a", "value"), property);
+    }
+
+    @Test
+    void testDoubleStringEmptyValues() {
+        final Object property = yamlConfiguration.getProperty("key5.example1");
         assertEquals(Arrays.asList("", "", "value"), property);
     }
 
diff --git 
a/src/test/resources/org/apache/commons/configuration2/yaml/cycle.yaml 
b/src/test/resources/org/apache/commons/configuration2/yaml/cycle.yaml
new file mode 100644
index 000000000..a00023265
--- /dev/null
+++ b/src/test/resources/org/apache/commons/configuration2/yaml/cycle.yaml
@@ -0,0 +1,17 @@
+# 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
+#
+#      https://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.
+
+root: &cycle
+  - *cycle
diff --git a/src/test/resources/test.yaml b/src/test/resources/test.yaml
index 6052e50aa..ee972d3dd 100644
--- a/src/test/resources/test.yaml
+++ b/src/test/resources/test.yaml
@@ -26,4 +26,5 @@ martin:
     skill: Elite
 
 key5:
-  example: [ '', '', value]
\ No newline at end of file
+  example1: [ '', '', value]
+  example2: [ 'a', 'a', value]  
\ No newline at end of file

Reply via email to