This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/reference-expansions in repository https://gitbox.apache.org/repos/asf/ws-neethi.git
commit dfcbf599f7a5fa63a9dbb71f84bcf6ba67a369f0 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Wed Aug 19 09:28:11 2026 +0100 Put limit on reference expansions --- .../org/apache/neethi/AbstractPolicyOperator.java | 56 +++++++++-- .../neethi/PolicyReferenceExpansionDoSTest.java | 102 +++++++++++++++++++++ 2 files changed, 149 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/apache/neethi/AbstractPolicyOperator.java b/src/main/java/org/apache/neethi/AbstractPolicyOperator.java index f55acbb..e4e4555 100644 --- a/src/main/java/org/apache/neethi/AbstractPolicyOperator.java +++ b/src/main/java/org/apache/neethi/AbstractPolicyOperator.java @@ -42,6 +42,18 @@ public abstract class AbstractPolicyOperator implements PolicyOperator { * fast, predictable RuntimeException. */ private static final int MAX_ALTERNATIVES = 10_000; + + /** + * Maximum number of PolicyReference expansions a single normalization + * pass may perform. The alternative-count cap alone cannot see reference + * re-expansion: a reference DAG in which every level holds two sibling + * references to the next level normalizes to a single alternative at each + * step while the expansion work doubles per level (2^d work from O(d) + * parsed elements, because the on-path cycle token is removed once each + * expansion completes). Budgeting expansions converts that unbounded + * work into a fast, predictable RuntimeException. + */ + private static final int MAX_REFERENCE_EXPANSIONS = 100_000; public AbstractPolicyOperator() { @@ -95,7 +107,7 @@ public abstract class AbstractPolicyOperator implements PolicyOperator { } - result.addPolicyComponent(normalizeOperator(policy, policy, reg, deep, new HashSet<String>())); + result.addPolicyComponent(normalizeOperator(policy, policy, reg, deep, new NormalizeBudget())); return result; } @@ -103,7 +115,7 @@ public abstract class AbstractPolicyOperator implements PolicyOperator { PolicyOperator operator, PolicyRegistry reg, boolean deep, - Set<String> resolving) { + NormalizeBudget budget) { short type = operator.getType(); @@ -160,26 +172,24 @@ public abstract class AbstractPolicyOperator implements PolicyOperator { String resolvedId = ((Policy) policyComponent).getId(); String resolvingToken = resolvedId != null && resolvedId.length() > 0 ? "id:" + resolvedId : "uri:" + uri; - if (!resolving.add(resolvingToken)) { - throw new RuntimeException("Circular PolicyReference detected: " + resolvingToken); - } + budget.enterReference(resolvingToken); try { All all = new All(); all.addPolicyComponents(((Policy) policyComponent).getPolicyComponents()); - childComponentsList.add(AbstractPolicyOperator.normalizeOperator(policy, all, reg, deep, resolving)); + childComponentsList.add(AbstractPolicyOperator.normalizeOperator(policy, all, reg, deep, budget)); } finally { - resolving.remove(resolvingToken); + budget.exitReference(resolvingToken); } } else if (policyComponent.getType() == Constants.TYPE_POLICY) { All all = new All(); all.addPolicyComponents(((Policy) policyComponent).getPolicyComponents()); - childComponentsList.add(AbstractPolicyOperator.normalizeOperator(policy, all, reg, deep, resolving)); + childComponentsList.add(AbstractPolicyOperator.normalizeOperator(policy, all, reg, deep, budget)); } else { childComponentsList.add(AbstractPolicyOperator .normalizeOperator(policy, - (PolicyOperator)policyComponent, reg, deep, resolving)); + (PolicyOperator)policyComponent, reg, deep, budget)); } } @@ -263,6 +273,34 @@ public abstract class AbstractPolicyOperator implements PolicyOperator { return crossProduct; } + /** + * Per-normalization-pass accounting: the on-path cycle set plus the + * budget that bounds total reference-expansion work. + */ + private static final class NormalizeBudget { + private final Set<String> resolving = new HashSet<String>(); + private long referenceExpansions; + + void enterReference(String token) { + if (!resolving.add(token)) { + throw new RuntimeException("Circular PolicyReference detected: " + token); + } + referenceExpansions++; + if (referenceExpansions > MAX_REFERENCE_EXPANSIONS) { + resolving.remove(token); + throw new RuntimeException( + "Policy normalization exceeded the maximum number of PolicyReference" + + " expansions (" + MAX_REFERENCE_EXPANSIONS + "). The policy may be" + + " crafted to cause Algorithmic Complexity DoS via reference" + + " re-expansion."); + } + } + + void exitReference(String token) { + resolving.remove(token); + } + } + public static void checkMaximumAlternativeCount(long alternativesCount, String operation) { if (alternativesCount > MAX_ALTERNATIVES) { throw new RuntimeException( diff --git a/src/test/java/org/apache/neethi/PolicyReferenceExpansionDoSTest.java b/src/test/java/org/apache/neethi/PolicyReferenceExpansionDoSTest.java new file mode 100644 index 0000000..9c00cb8 --- /dev/null +++ b/src/test/java/org/apache/neethi/PolicyReferenceExpansionDoSTest.java @@ -0,0 +1,102 @@ +/** + * 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; + +/** + * A reference DAG in which every level holds two sibling references to the + * next level normalizes to a single alternative at every step (so the + * 10000-alternatives cap never trips) while the expansion work doubles per + * level: 2^d normalizeOperator invocations from O(d) parsed elements, because + * the cycle token is removed once each expansion completes and sibling + * references then re-expand the same policy in full. The expansion budget + * converts that into a fast, predictable RuntimeException. + */ +public class PolicyReferenceExpansionDoSTest extends PolicyTestCase { + + private static final int DAG_DEPTH = 30; + + private static final String WSU_NS = + "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"; + + @Test + public void testDoublingReferenceDagIsRejectedByExpansionBudget() { + Policy policy = policyEngine.getPolicy(xmlStream(buildDoublingReferenceDagXml(DAG_DEPTH))); + + try { + policy.normalize(registry, true); + fail("Expected RuntimeException due to reference-expansion budget"); + } catch (RuntimeException ex) { + assertTrue(ex.getMessage().contains("PolicyReference expansions")); + } + } + + @Test + public void testSiblingReferencesToSamePolicyStillNormalize() { + Policy policy = policyEngine.getPolicy(xmlStream(buildSiblingReferencePolicyXml())); + + assertNotNull(policy.normalize(registry, true)); + } + + private static InputStream xmlStream(String xml) { + return new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)); + } + + private static String buildDoublingReferenceDagXml(int depth) { + StringBuilder xml = new StringBuilder(1024 + depth * 160); + xml.append("<wsp:Policy xmlns:wsp=\"http://www.w3.org/ns/ws-policy\"") + .append(" xmlns:wsu=\"").append(WSU_NS).append("\"") + .append(" xmlns:x=\"urn:x\">"); + + xml.append("<wsp:PolicyReference URI=\"#B1\"/>") + .append("<wsp:PolicyReference URI=\"#B1\"/>"); + + for (int i = 1; i < depth; i++) { + xml.append("<wsp:Policy wsu:Id=\"B").append(i).append("\">") + .append("<wsp:PolicyReference URI=\"#B").append(i + 1).append("\"/>") + .append("<wsp:PolicyReference URI=\"#B").append(i + 1).append("\"/>") + .append("</wsp:Policy>"); + } + + xml.append("<wsp:Policy wsu:Id=\"B").append(depth).append("\">") + .append("<x:Leaf/>") + .append("</wsp:Policy>"); + + xml.append("</wsp:Policy>"); + return xml.toString(); + } + + private static String buildSiblingReferencePolicyXml() { + StringBuilder xml = new StringBuilder(512); + xml.append("<wsp:Policy xmlns:wsp=\"http://www.w3.org/ns/ws-policy\"") + .append(" xmlns:wsu=\"").append(WSU_NS).append("\"") + .append(" xmlns:x=\"urn:x\">") + .append("<wsp:PolicyReference URI=\"#P\"/>") + .append("<wsp:PolicyReference URI=\"#P\"/>") + .append("<wsp:Policy wsu:Id=\"P\"><x:Leaf/></wsp:Policy>") + .append("</wsp:Policy>"); + return xml.toString(); + } +}
