This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/public-parser in repository https://gitbox.apache.org/repos/asf/ws-neethi.git
commit d21f0a79a365b2c965016d7761bd9b5343439b5e Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Wed Aug 19 09:17:20 2026 +0100 Fixing recursion case for nested policy --- src/main/java/org/apache/neethi/PolicyBuilder.java | 51 ++++++++++++-- .../neethi/PolicyBuilderNestedPolicyDoSTest.java | 81 ++++++++++++++++++++++ 2 files changed, 127 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/apache/neethi/PolicyBuilder.java b/src/main/java/org/apache/neethi/PolicyBuilder.java index 7941c7c..e208fc9 100644 --- a/src/main/java/org/apache/neethi/PolicyBuilder.java +++ b/src/main/java/org/apache/neethi/PolicyBuilder.java @@ -52,6 +52,18 @@ public class PolicyBuilder { private final int maxDepth; private final int maxElements; private final int maxAttributes; + + /** + * Budget of the policy parse in progress on the current thread. Assertion + * builders may re-enter the public getPolicy overloads for nested + * wsp:Policy elements (see XMLPrimitiveAssertionBuilder); such re-entrant + * parses must inherit the ambient budget rather than mint a fresh one, + * otherwise every assertion/wsp:Policy nesting layer resets the + * maxDepth/maxElements/maxAttributes budgets and the mutual recursion is + * unbounded. + */ + private static final ThreadLocal<ParseBudgetContext> CURRENT_BUDGET = + new ThreadLocal<ParseBudgetContext>(); public PolicyBuilder() { factory = new AssertionBuilderFactoryImpl(this); @@ -125,14 +137,12 @@ public class PolicyBuilder { } public Policy getPolicy(Element el) { - ParseBudgetContext context = new ParseBudgetContext(maxDepth, maxElements, maxAttributes); - return getPolicyOperator(el, context, 1); + return parseWithBudget(el); } public Policy getPolicy(XMLStreamReader reader) { - ParseBudgetContext context = new ParseBudgetContext(maxDepth, maxElements, maxAttributes); - return getPolicyOperator(reader, context, 1); + return parseWithBudget(reader); } /** @@ -143,8 +153,24 @@ public class PolicyBuilder { * @return a Policy object of the Policy element */ public Policy getPolicy(Object element) { + return parseWithBudget(element); + } + + private Policy parseWithBudget(Object element) { + ParseBudgetContext ambient = CURRENT_BUDGET.get(); + if (ambient != null) { + // Re-entrant parse: an assertion builder called back into the + // engine for a nested wsp:Policy element. Continue the enclosing + // parse's budget and depth instead of starting a fresh one. + return getPolicyOperator(element, ambient, ambient.getReentryDepth()); + } ParseBudgetContext context = new ParseBudgetContext(maxDepth, maxElements, maxAttributes); - return getPolicyOperator(element, context, 1); + CURRENT_BUDGET.set(context); + try { + return getPolicyOperator(element, context, 1); + } finally { + CURRENT_BUDGET.remove(); + } } /** @@ -263,9 +289,13 @@ public class PolicyBuilder { } else if (Constants.ELEM_POLICY_REF.equals(qn.getLocalPart())) { operator.addPolicyComponent(getPolicyReference(childElement, context, depth + 1)); } else { + // a nested wsp:Policy inside this assertion re-enters + // getPolicy below the assertion element itself + context.recordReentryDepth(depth + 2); operator.addPolicyComponent(factory.build(childElement)); } } else { + context.recordReentryDepth(depth + 2); operator.addPolicyComponent(factory.build(childElement)); } } @@ -291,6 +321,9 @@ public class PolicyBuilder { private final int maxAttributes; private int elementCount; private int attributeCount; + // depth at which a re-entrant getPolicy call (from an assertion + // builder) resumes; recorded just before each factory.build call + private int reentryDepth = 2; ParseBudgetContext(int maxDepth, int maxElements, int maxAttributes) { this.maxDepth = maxDepth; @@ -298,6 +331,14 @@ public class PolicyBuilder { this.maxAttributes = maxAttributes; } + void recordReentryDepth(int depth) { + reentryDepth = depth; + } + + int getReentryDepth() { + return reentryDepth; + } + void checkDepth(int depth) { if (depth > maxDepth) { throw new RuntimeException( diff --git a/src/test/java/org/apache/neethi/PolicyBuilderNestedPolicyDoSTest.java b/src/test/java/org/apache/neethi/PolicyBuilderNestedPolicyDoSTest.java new file mode 100644 index 0000000..9fe09e3 --- /dev/null +++ b/src/test/java/org/apache/neethi/PolicyBuilderNestedPolicyDoSTest.java @@ -0,0 +1,81 @@ +/** + * 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.nio.charset.StandardCharsets; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.w3c.dom.Element; + +import org.junit.Test; + +/** + * Reproduces unbounded recursion when a primitive assertion contains only a + * nested policy and the assertion builder re-enters the public policy parser. + */ +public class PolicyBuilderNestedPolicyDoSTest extends PolicyTestCase { + + private static final int SANDWICH_LAYERS = 100000; + + @Test + public void testNestedPolicyAssertionsAreRejectedByDepthBudget() { + PolicyBuilder builder = new PolicyBuilder(); + + try { + builder.getPolicy(parseElement(buildPolicyAssertionSandwichXml(SANDWICH_LAYERS))); + fail("Expected RuntimeException due to policy depth budget"); + } catch (RuntimeException ex) { + assertTrue(ex.getMessage().contains("maximum policy nesting depth")); + } + } + + private static Element parseElement(String xml) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + return factory.newDocumentBuilder().parse( + new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))).getDocumentElement(); + } catch (Exception ex) { + throw new RuntimeException("Could not parse test policy.", ex); + } + } + + private static String buildPolicyAssertionSandwichXml(int layers) { + StringBuilder xml = new StringBuilder(128 + (layers * 40)); + xml.append("<wsp:Policy xmlns:wsp=\"http://www.w3.org/ns/ws-policy\" ") + .append("xmlns:p=\"urn:neethi-test\">"); + + for (int i = 0; i < layers; i++) { + xml.append("<p:Assertion><wsp:Policy>"); + } + xml.append("<p:Assertion/>"); + for (int i = 0; i < layers; i++) { + xml.append("</wsp:Policy></p:Assertion>"); + } + + xml.append("</wsp:Policy>"); + return xml.toString(); + } +} \ No newline at end of file
