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(); + } +}
