This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/policy-alternatives in repository https://gitbox.apache.org/repos/asf/ws-neethi.git
commit d356edef0df1a94cc5110644bda73c8d08094792 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Jul 9 17:06:43 2026 +0100 normalization alternative limit can be bypassed through ExactlyOne aggregation --- .../org/apache/neethi/AbstractPolicyOperator.java | 25 +++++++--- .../apache/neethi/PolicyNormalizationDoSTest.java | 53 ++++++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/apache/neethi/AbstractPolicyOperator.java b/src/main/java/org/apache/neethi/AbstractPolicyOperator.java index b4f920c..491ee27 100644 --- a/src/main/java/org/apache/neethi/AbstractPolicyOperator.java +++ b/src/main/java/org/apache/neethi/AbstractPolicyOperator.java @@ -192,9 +192,13 @@ public abstract class AbstractPolicyOperator implements PolicyOperator { ExactlyOne exactlyOne = new ExactlyOne(); if (componentType == Constants.TYPE_EXACTLYONE) { + long alternativesSoFar = 0; for (PolicyComponent comp : normalizedInnerComponets) { ExactlyOne innerExactlyOne = (ExactlyOne)comp; + int alternativesToAdd = innerExactlyOne.getPolicyComponents().size(); + checkAlternativeBudget(alternativesSoFar + alternativesToAdd); exactlyOne.addPolicyComponents(innerExactlyOne.getPolicyComponents()); + alternativesSoFar += alternativesToAdd; } } else if ((componentType == Constants.TYPE_POLICY) || (componentType == Constants.TYPE_ALL)) { @@ -228,6 +232,8 @@ public abstract class AbstractPolicyOperator implements PolicyOperator { exactlyOne = (ExactlyOne) normalizedInnerComponets.get(0); } } + + checkAlternativeBudget(exactlyOne.getPolicyComponents().size()); return exactlyOne; } @@ -243,21 +249,26 @@ public abstract class AbstractPolicyOperator implements PolicyOperator { currentAll1 = (All)pc; for (PolicyComponent pc2 : exactlyOne2.getPolicyComponents()) { + long nextSize = (long)crossProduct.getPolicyComponents().size() + 1; + checkAlternativeBudget(nextSize); + currentAll2 = (All)pc2; crossProductAll = new All(); crossProductAll.addPolicyComponents(currentAll1.getPolicyComponents()); crossProductAll.addPolicyComponents(currentAll2.getPolicyComponents()); crossProduct.addPolicyComponent(crossProductAll); - - if (crossProduct.getPolicyComponents().size() > MAX_ALTERNATIVES) { - throw new RuntimeException( - "Policy normalization exceeded the maximum number of alternatives (" - + MAX_ALTERNATIVES + "). The policy may be crafted to cause " - + "Algorithmic Complexity DoS via exponential cross-product expansion."); - } } } return crossProduct; } + + private static void checkAlternativeBudget(long alternativesCount) { + if (alternativesCount > MAX_ALTERNATIVES) { + throw new RuntimeException( + "Policy normalization exceeded the maximum number of alternatives (" + + MAX_ALTERNATIVES + "). The policy may be crafted to cause " + + "Algorithmic Complexity DoS via exponential expansion."); + } + } } diff --git a/src/test/java/org/apache/neethi/PolicyNormalizationDoSTest.java b/src/test/java/org/apache/neethi/PolicyNormalizationDoSTest.java index 195f54e..dfbf351 100644 --- a/src/test/java/org/apache/neethi/PolicyNormalizationDoSTest.java +++ b/src/test/java/org/apache/neethi/PolicyNormalizationDoSTest.java @@ -77,6 +77,29 @@ public class PolicyNormalizationDoSTest extends PolicyTestCase { policy.normalize(registry, true); } + /** + * Asserts that the alternative budget is enforced globally even when the final + * count is reached through ExactlyOne aggregation, not cross-product generation. + * + * <pre> + * Policy + * ExactlyOne + * All -> (13 binary ExactlyOne nodes) => 8192 alternatives + * All -> (13 binary ExactlyOne nodes) => 8192 alternatives + * </pre> + */ + @Test + public void testExactlyOneAggregationEnforcesGlobalAlternativeLimit() { + Policy policy = buildExactlyOneAggregationBypassPolicy(2, 13); + + try { + policy.normalize(registry, true); + fail("Expected RuntimeException due to global alternative limit"); + } catch (RuntimeException ex) { + assertTrue(ex.getMessage().contains("maximum number of alternatives")); + } + } + // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- @@ -113,4 +136,34 @@ public class PolicyNormalizationDoSTest extends PolicyTestCase { return policy; } + private static Policy buildExactlyOneAggregationBypassPolicy(int branchCount, int branchDepth) { + Policy policy = new Policy(); + ExactlyOne outerExactlyOne = new ExactlyOne(); + + for (int branch = 0; branch < branchCount; branch++) { + All outerBranch = new All(); + + for (int depth = 0; depth < branchDepth; depth++) { + ExactlyOne binaryChoice = new ExactlyOne(); + + All branchA = new All(); + branchA.addPolicyComponent( + new PrimitiveAssertion(new QName("urn:test", "branch" + branch + "a" + depth))); + + All branchB = new All(); + branchB.addPolicyComponent( + new PrimitiveAssertion(new QName("urn:test", "branch" + branch + "b" + depth))); + + binaryChoice.addPolicyComponent(branchA); + binaryChoice.addPolicyComponent(branchB); + outerBranch.addPolicyComponent(binaryChoice); + } + + outerExactlyOne.addPolicyComponent(outerBranch); + } + + policy.addPolicyComponent(outerExactlyOne); + return policy; + } + }
