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

coheigea pushed a commit to branch coheigea/policy-builder
in repository https://gitbox.apache.org/repos/asf/ws-neethi.git

commit a6091462d116e6e7ff9de776d4878c9fafa4cc89
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Jul 10 10:29:01 2026 +0100

    Add limits for max depth, attributes, elements in a policy
---
 src/main/java/org/apache/neethi/PolicyBuilder.java | 123 ++++++++++++++++++---
 .../org/apache/neethi/PolicyBuilderDoSTest.java    |  80 ++++++++++++++
 2 files changed, 189 insertions(+), 14 deletions(-)

diff --git a/src/main/java/org/apache/neethi/PolicyBuilder.java 
b/src/main/java/org/apache/neethi/PolicyBuilder.java
index 79ee02f..c5f81d0 100644
--- a/src/main/java/org/apache/neethi/PolicyBuilder.java
+++ b/src/main/java/org/apache/neethi/PolicyBuilder.java
@@ -39,15 +39,32 @@ import org.apache.neethi.builders.AssertionBuilder;
  */
 public class PolicyBuilder {
 
+    private static final String MAX_DEPTH_PROPERTY = 
"org.apache.neethi.parser.maxDepth";
+    private static final String MAX_ELEMENTS_PROPERTY = 
"org.apache.neethi.parser.maxElements";
+    private static final String MAX_ATTRIBUTES_PROPERTY = 
"org.apache.neethi.parser.maxAttributes";
+
+    private static final int DEFAULT_MAX_DEPTH = 256;
+    private static final int DEFAULT_MAX_ELEMENTS = 100000;
+    private static final int DEFAULT_MAX_ATTRIBUTES = 10000;
+
     protected AssertionBuilderFactory factory;
     protected PolicyRegistry defaultPolicyRegistry;
+    private final int maxDepth;
+    private final int maxElements;
+    private final int maxAttributes;
     
     public PolicyBuilder() {
         factory = new AssertionBuilderFactoryImpl(this);
+        maxDepth = readConfiguredLimit(MAX_DEPTH_PROPERTY, DEFAULT_MAX_DEPTH);
+        maxElements = readConfiguredLimit(MAX_ELEMENTS_PROPERTY, 
DEFAULT_MAX_ELEMENTS);
+        maxAttributes = readConfiguredLimit(MAX_ATTRIBUTES_PROPERTY, 
DEFAULT_MAX_ATTRIBUTES);
     }
     
     public PolicyBuilder(AssertionBuilderFactory factory) {
         this.factory = factory;
+        maxDepth = readConfiguredLimit(MAX_DEPTH_PROPERTY, DEFAULT_MAX_DEPTH);
+        maxElements = readConfiguredLimit(MAX_ELEMENTS_PROPERTY, 
DEFAULT_MAX_ELEMENTS);
+        maxAttributes = readConfiguredLimit(MAX_ATTRIBUTES_PROPERTY, 
DEFAULT_MAX_ATTRIBUTES);
     }
     
     
@@ -108,12 +125,14 @@ public class PolicyBuilder {
     }
 
     public Policy getPolicy(Element el) {
-        return getPolicyOperator(el);
+        ParseBudgetContext context = new ParseBudgetContext(maxDepth, 
maxElements, maxAttributes);
+        return getPolicyOperator(el, context, 1);
     }
     
     
     public Policy getPolicy(XMLStreamReader reader) {
-        return getPolicyOperator(reader);
+        ParseBudgetContext context = new ParseBudgetContext(maxDepth, 
maxElements, maxAttributes);
+        return getPolicyOperator(reader, context, 1);
     }
 
     /**
@@ -124,7 +143,8 @@ public class PolicyBuilder {
      * @return a Policy object of the Policy element
      */
     public Policy getPolicy(Object element) {
-        return getPolicyOperator(element);
+        ParseBudgetContext context = new ParseBudgetContext(maxDepth, 
maxElements, maxAttributes);
+        return getPolicyOperator(element, context, 1);
     }
 
     /**
@@ -156,6 +176,14 @@ public class PolicyBuilder {
      * @return a PolicyReference object of the PolicyReference element
      */
     public PolicyReference getPolicyReference(Object element) {
+        ParseBudgetContext context = new ParseBudgetContext(maxDepth, 
maxElements, maxAttributes);
+        return getPolicyReference(element, context, 1);
+    }
+
+    private PolicyReference getPolicyReference(Object element, 
ParseBudgetContext context, int depth) {
+        context.checkDepth(depth);
+        context.incrementElementCount();
+
         QName qn = factory.getConverterRegistry().findQName(element);
 
         if (!Constants.isPolicyRef(qn)) {
@@ -166,37 +194,48 @@ public class PolicyBuilder {
         PolicyReference reference = new PolicyReference(this);
 
         Map<QName, String> attributes = 
factory.getConverterRegistry().getAttributes(element);
+        context.incrementAttributeCount(attributes.size());
 
         // setting the URI value
         reference.setURI(attributes.get(new QName("URI")));
         return reference;
     }
 
-    private Policy getPolicyOperator(Object element) {
+    private Policy getPolicyOperator(Object element, ParseBudgetContext 
context, int depth) {
+        context.checkDepth(depth);
+        context.incrementElementCount();
+
         QName qn = factory.getConverterRegistry().findQName(element);
         
         if (Constants.isPolicyElement(qn)) {
             String ns = qn.getNamespaceURI();
-            return (Policy) processOperationElement(element, new 
Policy(defaultPolicyRegistry, ns));
+            return (Policy) processOperationElement(element, new 
Policy(defaultPolicyRegistry, ns), context, depth);
         }
         throw new IllegalArgumentException(qn + " is not a <wsp:Policy> 
element."); 
     }
 
-    private ExactlyOne getExactlyOneOperator(Object element) {
-        return (ExactlyOne) processOperationElement(element, new ExactlyOne());
+    private ExactlyOne getExactlyOneOperator(Object element, 
ParseBudgetContext context, int depth) {
+        context.checkDepth(depth);
+        context.incrementElementCount();
+        return (ExactlyOne) processOperationElement(element, new ExactlyOne(), 
context, depth);
     }
 
-    private All getAllOperator(Object element) {
-        return (All) processOperationElement(element, new All());
+    private All getAllOperator(Object element, ParseBudgetContext context, int 
depth) {
+        context.checkDepth(depth);
+        context.incrementElementCount();
+        return (All) processOperationElement(element, new All(), context, 
depth);
     }
 
     private PolicyOperator processOperationElement(Object operationElement,
-                                                   PolicyOperator operator) {
+                                                   PolicyOperator operator,
+                                                   ParseBudgetContext context,
+                                                   int depth) {
 
         if (Constants.TYPE_POLICY == operator.getType()) {
             Policy policyOperator = (Policy) operator;
 
             Map<QName, String> attributes = 
factory.getConverterRegistry().getAttributes(operationElement);
+            context.incrementAttributeCount(attributes.size());
             
             for (Map.Entry<QName, String> ent : attributes.entrySet()) {
                 policyOperator.addAttribute(ent.getKey(), ent.getValue());
@@ -216,13 +255,13 @@ public class PolicyBuilder {
                 
             } else if (Constants.isInPolicyNS(qn)) {
                 if (Constants.ELEM_POLICY.equals(qn.getLocalPart())) {
-                    
operator.addPolicyComponent(getPolicyOperator(childElement));
+                    
operator.addPolicyComponent(getPolicyOperator(childElement, context, depth + 
1));
                 } else if 
(Constants.ELEM_EXACTLYONE.equals(qn.getLocalPart())) {
-                    
operator.addPolicyComponent(getExactlyOneOperator(childElement));
+                    
operator.addPolicyComponent(getExactlyOneOperator(childElement, context, depth 
+ 1));
                 } else if (Constants.ELEM_ALL.equals(qn.getLocalPart())) {
-                    operator.addPolicyComponent(getAllOperator(childElement));
+                    operator.addPolicyComponent(getAllOperator(childElement, 
context, depth + 1));
                 } else if 
(Constants.ELEM_POLICY_REF.equals(qn.getLocalPart())) {
-                    
operator.addPolicyComponent(getPolicyReference(childElement));
+                    
operator.addPolicyComponent(getPolicyReference(childElement, context, depth + 
1));
                 } else {
                     operator.addPolicyComponent(factory.build(childElement));
                 }
@@ -232,6 +271,62 @@ public class PolicyBuilder {
         }
         return operator;
     } 
+
+    private static int readConfiguredLimit(String key, int defaultValue) {
+        String value = System.getProperty(key);
+        if (value == null || value.trim().length() == 0) {
+            return defaultValue;
+        }
+        try {
+            int parsed = Integer.parseInt(value.trim());
+            return parsed > 0 ? parsed : defaultValue;
+        } catch (NumberFormatException ex) {
+            return defaultValue;
+        }
+    }
+
+    private static final class ParseBudgetContext {
+        private final int maxDepth;
+        private final int maxElements;
+        private final int maxAttributes;
+        private int elementCount;
+        private int attributeCount;
+
+        ParseBudgetContext(int maxDepth, int maxElements, int maxAttributes) {
+            this.maxDepth = maxDepth;
+            this.maxElements = maxElements;
+            this.maxAttributes = maxAttributes;
+        }
+
+        void checkDepth(int depth) {
+            if (depth > maxDepth) {
+                throw new RuntimeException(
+                    "Policy parsing exceeded the maximum policy nesting depth 
("
+                    + maxDepth + ").");
+            }
+        }
+
+        void incrementElementCount() {
+            elementCount++;
+            if (elementCount > maxElements) {
+                throw new RuntimeException(
+                    "Policy parsing exceeded the maximum number of elements ("
+                    + maxElements + ").");
+            }
+        }
+
+        void incrementAttributeCount(int delta) {
+            if (delta <= 0) {
+                return;
+            }
+            attributeCount += delta;
+            if (attributeCount > maxAttributes) {
+                throw new RuntimeException(
+                    "Policy parsing exceeded the maximum number of attributes 
("
+                    + maxAttributes + ").");
+            }
+        }
+    }
     
     protected void notifyUnknownPolicyElement(Object childElement) {
         //NO-Op - subclass could log or throw exception or something
diff --git a/src/test/java/org/apache/neethi/PolicyBuilderDoSTest.java 
b/src/test/java/org/apache/neethi/PolicyBuilderDoSTest.java
new file mode 100644
index 0000000..df26f23
--- /dev/null
+++ b/src/test/java/org/apache/neethi/PolicyBuilderDoSTest.java
@@ -0,0 +1,80 @@
+/**
+ * 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.neethi;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.Test;
+
+/**
+ * Reproduces parser recursion DoS where deeply nested policy operators trigger
+ * StackOverflowError during PolicyBuilder.getPolicy(InputStream).
+ */
+public class PolicyBuilderDoSTest extends PolicyTestCase {
+
+    private static final int DEPTH_WITHIN_DEFAULT_BUDGET = 64;
+    private static final int DEPTH_ABOVE_DEFAULT_BUDGET = 4096;
+
+    @Test
+    public void testDeeplyNestedPolicyIsRejectedByDepthBudget() {
+        PolicyBuilder builder = new PolicyBuilder();
+        InputStream stream = 
xmlStream(buildNestedAllPolicyXml(DEPTH_ABOVE_DEFAULT_BUDGET));
+
+        try {
+            builder.getPolicy(stream);
+            fail("Expected RuntimeException due to policy depth budget");
+        } catch (RuntimeException ex) {
+            assertTrue(ex.getMessage().contains("maximum policy nesting 
depth"));
+        }
+    }
+
+    @Test
+    public void testNestedPolicyWithinDepthBudgetParses() {
+        PolicyBuilder builder = new PolicyBuilder();
+        InputStream stream = 
xmlStream(buildNestedAllPolicyXml(DEPTH_WITHIN_DEFAULT_BUDGET));
+        Policy policy = builder.getPolicy(stream);
+
+        assertNotNull(policy);
+    }
+
+    private static InputStream xmlStream(String xml) {
+        return new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private static String buildNestedAllPolicyXml(int depth) {
+        StringBuilder xml = new StringBuilder(128 + (depth * 20));
+        xml.append("<wsp:Policy 
xmlns:wsp=\"http://www.w3.org/ns/ws-policy\";>");
+
+        for (int i = 0; i < depth; i++) {
+            xml.append("<wsp:All>");
+        }
+
+        xml.append("<wsp:All/>");
+
+        for (int i = 0; i < depth; i++) {
+            xml.append("</wsp:All>");
+        }
+
+        xml.append("</wsp:Policy>");
+        return xml.toString();
+    }
+}

Reply via email to