Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,12 @@ Policy normalization also enforces several hard caps:
MAX_ALTERNATIVES can still materialize hundreds of millions of references
(alternatives × parent widths). This cap ensures a fast RuntimeException
instead of OutOfMemoryError.

`PolicyComparator` also enforces a comparison budget:

- `MAX_COMPARISONS` - maximum number of pairwise component comparisons a
single top-level `compare(...)` call may perform.
Default: `10000000`. `PolicyComparator`'s list matching is unordered and
unmemoized, so mismatched operand orderings cost O(n1 * n2) comparisons.
This cap turns an engineered quadratic comparison into a fast, predictable
RuntimeException instead of pinned CPU.
19 changes: 18 additions & 1 deletion THREAT-MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,22 @@ overload disables both. The pre-parsed-`Element` / `XMLStreamReader` /
divergence is security-meaningful downstream.
- *(inferred — §14 Q10)*

### P12 — `PolicyComparator` pairwise-comparison budget is enforced

- **Condition**: `PolicyComparator.compare(...)` (directly, or
transitively via `Policy.equals` / `AbstractPolicyOperator.equal` /
`PolicyContainingPrimitiveAssertion.equal`) is invoked on two
well-formed `PolicyComponent` trees.
- **Violation symptom**: the unordered, unmemoized list-matching in
`compare(List, List)` completes more than `MAX_COMPARISONS` pairwise
component comparisons without rejection — an adversarial operand
ordering costs O(n1 * n2) comparisons with no cap, up to minutes of
pinned CPU at the parser budgets.
- **Severity**: **availability-relevant** (quadratic, not exponential —
bounded even unpatched), `VALID` per §13.
- *(documented: `README.txt` — `MAX_COMPARISONS`, default `10000000`;
`src/main/java/org/apache/neethi/util/PolicyComparator.java`)*

## §9 Security properties the project does *not* provide

State each plainly so a triager can route an inbound report to the
Expand Down Expand Up @@ -796,6 +812,7 @@ source comments. The project website is
| --- | --- | --- |
| `README.txt` | "implementation of WS-Policy Specification (September, 2007)"; "It provides a convenient model and an API to process policy information at runtime and an extension model for serialization and de-serialization of domain-specific Assertions" | §1, §2 intended use |
| `README.txt` | documented security budgets: `org.apache.neethi.parser.maxDepth=256`, `org.apache.neethi.parser.maxElements=100000`, `org.apache.neethi.parser.maxAttributes=10000`, `org.apache.neethi.remote.maxPolicyBytes=67108864`, and normalization/intersection cap `10000` alternatives; invalid/unset values fall back to defaults | §5a, §6, §8 P2-P6, §10 item 4 |
| `README.txt` | `PolicyComparator` comparison budget: `MAX_COMPARISONS=10000000` pairwise component comparisons per top-level `compare(...)` call; throws `RuntimeException` on exhaustion | §8 P12 |
| `src/main/java/org/apache/neethi/PolicyBuilder.java` lines 99-100 (`getPolicy(InputStream)`) | `xif.setProperty(IS_SUPPORTING_EXTERNAL_ENTITIES, FALSE); xif.setProperty(SUPPORT_DTD, FALSE)` | §8 P1, §11a |
| `PolicyBuilder.java` lines 140-141 (`getPolicyReference(InputStream)`) | same XXE/DTD hardening on the PolicyReference parse path | §8 P1, §11a |
| `PolicyReference.java` lines 141-190 (`getRemoteReferencedPolicy(String u)`) | the remote-policy fetcher | §1 (deployment shape), §4 B5, §5 network, §5a, §8 P7-P10, §9 first three bullets, §10 items 2-3, §11 |
Expand All @@ -806,5 +823,5 @@ source comments. The project website is
| `PolicyEngine.java` lines 45-52 | "static synchronized PolicyBuilder" facade | §9 false-friend, §11 |
| `AssertionBuilderFactoryImpl.java`, `util.Service` | ServiceLoader-style discovery of `AssertionBuilder` via `META-INF/services/` | §5, §10 item 6 |
| `Policy.java`, `All.java`, `ExactlyOne.java`, `AbstractPolicyOperator.java` | `normalize(reg, deep)` resolves references via registry/local `#id` only and throws on a miss; remote fetch requires a direct embedder call to `PolicyReference.normalize(reg, deep)` or `getRemoteReferencedPolicy(...)` | §4 B4-B5, §11 |
| `util.PolicyIntersector`, `util.PolicyComparator` | policy-algebra utilities | §8 P11, §14 Q10 |
| `util.PolicyIntersector`, `util.PolicyComparator` | policy-algebra utilities | §8 P11-P12, §14 Q10 |
| `RELEASE-NOTE.txt` | release notes per version | §1 supported branches |
53 changes: 45 additions & 8 deletions src/main/java/org/apache/neethi/util/PolicyComparator.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ private PolicyComparator() {
//utility class
}

/**
* Maximum number of pairwise component comparisons a single top-level
* compare call may perform. The list comparison is unordered nested
* matching with no memoization, so mismatched operand orderings cost
* O(n1 * n2) comparisons, converting an engineered quadratic comparison
* into a fast, predictable RuntimeException instead of pinned CPU.
*/
private static final long MAX_COMPARISONS = 10_000_000L;

private static void chargeComparison(long[] budget) {
if (++budget[0] > MAX_COMPARISONS) {
throw new RuntimeException(
"Policy comparison exceeded the maximum number of component"
+ " comparisons (" + MAX_COMPARISONS + "). The operands may be"
+ " crafted to cause Algorithmic Complexity DoS via unordered"
+ " matching.");
}
}

/**
* Returns {@code true} if the two policies have the same semantics
*
Expand All @@ -47,6 +66,10 @@ private PolicyComparator() {
* @return {@code true} if both policies have the same semantics
*/
public static boolean compare(Policy arg1, Policy arg2) {
return compare(arg1, arg2, new long[1]);
}

private static boolean compare(Policy arg1, Policy arg2, long[] budget) {

// check Name attributes of each policies
if (arg1.getName() != null) {
Expand Down Expand Up @@ -79,7 +102,7 @@ public static boolean compare(Policy arg1, Policy arg2) {
}
}

return compare(arg1.getPolicyComponents(), arg2.getPolicyComponents());
return compare(arg1.getPolicyComponents(), arg2.getPolicyComponents(), budget);
}

/**
Expand All @@ -93,18 +116,23 @@ public static boolean compare(Policy arg1, Policy arg2) {
* @return {@code true} if both PolicyComponents have the same semantics
*/
public static boolean compare(PolicyComponent arg1, PolicyComponent arg2) {
return compare(arg1, arg2, new long[1]);
}

private static boolean compare(PolicyComponent arg1, PolicyComponent arg2, long[] budget) {
chargeComparison(budget);
if (!arg1.getClass().equals(arg2.getClass())) {
return false;
}

if (arg1 instanceof Policy) {
return compare((Policy) arg1, (Policy) arg2);
return compare((Policy) arg1, (Policy) arg2, budget);

} else if (arg1 instanceof All) {
return compare((All) arg1, (All) arg2);
return compare((All) arg1, (All) arg2, budget);

} else if (arg1 instanceof ExactlyOne) {
return compare((ExactlyOne) arg1, (ExactlyOne) arg2);
return compare((ExactlyOne) arg1, (ExactlyOne) arg2, budget);

} else if (arg1 instanceof Assertion) {
return compare((Assertion) arg1, (Assertion) arg2);
Expand All @@ -117,11 +145,19 @@ public static boolean compare(PolicyComponent arg1, PolicyComponent arg2) {
}

public static boolean compare(All arg1, All arg2) {
return compare(arg1.getPolicyComponents(), arg2.getPolicyComponents());
return compare(arg1, arg2, new long[1]);
}

private static boolean compare(All arg1, All arg2, long[] budget) {
return compare(arg1.getPolicyComponents(), arg2.getPolicyComponents(), budget);
}

public static boolean compare(ExactlyOne arg1, ExactlyOne arg2) {
return compare(arg1.getPolicyComponents(), arg2.getPolicyComponents());
return compare(arg1, arg2, new long[1]);
}

private static boolean compare(ExactlyOne arg1, ExactlyOne arg2, long[] budget) {
return compare(arg1.getPolicyComponents(), arg2.getPolicyComponents(), budget);
}

public static boolean compare(Assertion arg1, Assertion arg2) {
Expand All @@ -131,7 +167,8 @@ public static boolean compare(Assertion arg1, Assertion arg2) {
return true;
}

private static boolean compare(List<PolicyComponent> arg1, List<PolicyComponent> arg2) {
private static boolean compare(List<PolicyComponent> arg1, List<PolicyComponent> arg2,
long[] budget) {
if (arg1.size() != arg2.size()) {
return false;
}
Expand All @@ -140,7 +177,7 @@ private static boolean compare(List<PolicyComponent> arg1, List<PolicyComponent>
for (PolicyComponent assertion1 : arg1) {
boolean match = false;
for (PolicyComponent assertion2 : arg2) {
if (compare(assertion1, assertion2)) {
if (compare(assertion1, assertion2, budget)) {
match = true;
break;
}
Expand Down
83 changes: 83 additions & 0 deletions src/test/java/org/apache/neethi/util/PolicyComparatorDoSTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* 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.util;

import javax.xml.namespace.QName;

import org.apache.neethi.All;
import org.apache.neethi.ExactlyOne;
import org.apache.neethi.Policy;
import org.apache.neethi.PolicyTestCase;
import org.apache.neethi.builders.PrimitiveAssertion;

import org.junit.Test;

/**
* PolicyComparator's list comparison is unordered nested matching with no
* memoization: each component of one list scans the other list for a match,
* so operand orderings engineered to match late cost O(n1 * n2) comparisons
* — minutes of pinned CPU at the parse budgets. The comparison budget must
* turn that into a fast, predictable RuntimeException.
*/
public class PolicyComparatorDoSTest extends PolicyTestCase {

private static final int LARGE = 6000;
private static final int SMALL = 200;

@Test
public void testQuadraticUnorderedMatchingIsRejectedByComparisonBudget() {
Policy p1 = buildAlternativesPolicy(LARGE, false);
Policy p2 = buildAlternativesPolicy(LARGE, true);

try {
PolicyComparator.compare(p1, p2);
fail("Expected RuntimeException due to comparison budget");
} catch (RuntimeException ex) {
assertTrue(ex.getMessage().contains("component comparisons"));
}
}

@Test
public void testModerateUnorderedComparisonStillWorks() {
Policy p1 = buildAlternativesPolicy(SMALL, false);
Policy p2 = buildAlternativesPolicy(SMALL, true);

assertTrue(PolicyComparator.compare(p1, p2));
}

/**
* Builds a Policy holding one ExactlyOne of {@code n} single-assertion
* alternatives. With {@code reversed} set, the alternatives appear in
* reverse order, so the unordered matcher finds each partner only at the
* far end of its scan — the quadratic worst case for equal operands.
*/
private static Policy buildAlternativesPolicy(int n, boolean reversed) {
Policy policy = new Policy();
ExactlyOne eo = new ExactlyOne();
for (int i = 0; i < n; i++) {
int idx = reversed ? n - 1 - i : i;
All all = new All();
all.addPolicyComponent(new PrimitiveAssertion(new QName("urn:test", "a" + idx)));
eo.addPolicyComponent(all);
}
policy.addPolicyComponent(eo);
return policy;
}
}