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 32f408ce2251e22e554441aab486c1df93811382 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 ++++++++++++++++++---- 1 file changed, 47 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(
