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
41 changes: 29 additions & 12 deletions THREAT-MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,25 +284,33 @@ overload disables both. The pre-parsed-`Element` / `XMLStreamReader` /
`org.apache.neethi.parser.maxDepth=256`,
`org.apache.neethi.parser.maxElements=100000`,
`org.apache.neethi.parser.maxAttributes=10000` *(documented:
`README.txt`)*.
`README.txt` — implemented in `PolicyBuilder.java`, enforced
operator-walk and Stax-to-DOM materialization phases)*.
- Documented bound on remotely fetched policy size:
`org.apache.neethi.remote.maxPolicyBytes=67108864` bytes (`64 MiB`)
*(documented: `README.txt`)*.
*(documented: `README.txt` — bounds total bytes read, not element
budget; assertion subtrees from remote policies are still charged
against `maxElements` / `maxAttributes`)*.
- Documented hard cap of `10000` normalized policy alternatives during
normalization/intersection *(documented: `README.txt`)*.
- Documented hard cap of `1000000` assertion-pair intersection attempts
per top-level `PolicyIntersector.intersect(...)` /
`PolicyIntersector.compatiblePolicies(...)` call *(documented:
`README.txt`)*.
- No documented bound on the number of `PolicyReference` URIs in a
single policy *(inferred — §14 Q7)*. Transitive fetch chains cannot
- **No documented bound on the number of `PolicyReference` URIs in a
single policy** *(inferred — §14 Q7)*. Transitive fetch chains cannot
occur inside one normalization call: a fetched policy is
re-normalized through the registry-only path, which throws on any
further unresolved absolute reference *(inferred — §14 Q11)*.
- No rate limit on `getRemoteReferencedPolicy` fetches — each direct
further unresolved absolute reference *(inferred — §14 Q11)*. Note:
the element/attribute budgets (P3, P4) apply to all fetched policies
as well as local policies, so an embedded attacker cannot bypass them
via chained remote references.
- **No rate limit on `getRemoteReferencedPolicy` fetches** — each direct
embedder call to `PolicyReference.normalize(reg, deep)` on an
unresolved reference triggers one HTTP GET; `Policy.normalize(...)`
itself issues no fetches and throws on an unresolved reference.
Multiple fetches can amplify wall-clock latency but cannot amplify
memory consumption beyond `maxElements` / `maxAttributes`.
- Connect-timeout (5 s) and read-timeout (10 s) bound the wall-clock
per fetch, but an embedder that directly dereferences many distinct
unresolved references can multiply the latency *(inferred — §14 Q7)*.
Expand Down Expand Up @@ -347,23 +355,32 @@ overload disables both. The pre-parsed-`Element` / `XMLStreamReader` /
- *(documented: `README.txt` — `org.apache.neethi.parser.maxDepth`,
default `256`)*

### P3 — Parsed-element-count budget is enforced
### P3 — Parsed-element-count budget is enforced on all materialized elements

- **Condition**: policy parsing occurs through Neethi's parser path.
This includes the operator walk over `wsp:*` elements **and** the
Stax-to-DOM conversion of assertion subtrees (`StaxToDOMConverter`),
which materializes XML stream events into DOM nodes.
- **Violation symptom**: parser accepts more than configured/default
`org.apache.neethi.parser.maxElements` elements.
`org.apache.neethi.parser.maxElements` elements from *any* materialization
phase (operator walk or Stax conversion).
- **Severity**: **availability-relevant**, `VALID` per §13.
- *(documented: `README.txt` — `org.apache.neethi.parser.maxElements`,
default `100000`)*
default `100000`; implemented in `PolicyBuilder.java`,
`StaxToDOMConverter.java`)*

### P4 — Parsed-attribute-count budget is enforced
### P4 — Parsed-attribute-count budget is enforced on all materialized attributes

- **Condition**: policy parsing occurs through Neethi's parser path.
This includes attributes on `wsp:*` operators **and** attributes on
assertion elements materialized by Stax-to-DOM conversion.
- **Violation symptom**: parser accepts more than configured/default
`org.apache.neethi.parser.maxAttributes` attributes.
`org.apache.neethi.parser.maxAttributes` attributes from *any* materialization
phase (operator attributes or Stax-to-DOM conversion).
- **Severity**: **availability-relevant**, `VALID` per §13.
- *(documented: `README.txt` — `org.apache.neethi.parser.maxAttributes`,
default `10000`)*
default `10000`; implemented in `PolicyBuilder.java`,
`StaxToDOMConverter.java`)*

### P5 — Remote policy fetch byte budget is enforced

Expand Down
35 changes: 35 additions & 0 deletions src/main/java/org/apache/neethi/PolicyBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ private PolicyOperator processOperationElement(Object operationElement,
for (Map.Entry<QName, String> ent : attributes.entrySet()) {
policyOperator.addAttribute(ent.getKey(), ent.getValue());
}
} else if (Constants.TYPE_POLICY != operator.getType()) {
// attributes on ExactlyOne/All operator elements were previously
// exempt from the maxAttributes budget
Map<QName, String> operatorAttributes =
factory.getConverterRegistry().getAttributes(operationElement);
if (operatorAttributes != null) {
context.incrementAttributeCount(operatorAttributes.size());
}
}

for (Iterator<?> iterator = factory.getConverterRegistry().getChildElements(operationElement);
Expand All @@ -292,10 +300,12 @@ private PolicyOperator processOperationElement(Object operationElement,
// a nested wsp:Policy inside this assertion re-enters
// getPolicy below the assertion element itself
context.recordReentryDepth(depth + 2);
context.incrementElementCount();
operator.addPolicyComponent(factory.build(childElement));
}
} else {
context.recordReentryDepth(depth + 2);
context.incrementElementCount();
operator.addPolicyComponent(factory.build(childElement));
}
}
Expand Down Expand Up @@ -369,6 +379,31 @@ void incrementAttributeCount(int delta) {
}
}

/**
* Charges one materialized element against the budget of the policy parse
* in progress on the current thread, if any. Called by the converter
* layer when it copies an assertion subtree into a new representation, so
* that maxElements bounds every node materialized on behalf of a single
* top-level parse - not only wsp:* operator elements.
*/
public static void chargeAmbientElement() {
ParseBudgetContext context = CURRENT_BUDGET.get();
if (context != null) {
context.incrementElementCount();
}
}

/**
* Charges {@code count} materialized attributes against the budget of the
* policy parse in progress on the current thread, if any.
*/
public static void chargeAmbientAttributes(int count) {
ParseBudgetContext context = CURRENT_BUDGET.get();
if (context != null) {
context.incrementAttributeCount(count);
}
}

protected void notifyUnknownPolicyElement(Object childElement) {
//NO-Op - subclass could log or throw exception or something
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import org.apache.neethi.PolicyBuilder;

/**
*
*/
Expand Down Expand Up @@ -72,6 +74,12 @@ public static void readDocElements(Document doc, Node parent,
}
e = (Element)parent.appendChild(e);

// charge every node materialized for an assertion subtree
// against the budget of the parse that requested it; the
// operator walk in PolicyBuilder never sees these nodes
PolicyBuilder.chargeAmbientElement();
PolicyBuilder.chargeAmbientAttributes(reader.getAttributeCount());

for (int ns = 0; ns < reader.getNamespaceCount(); ns++) {
String uri = reader.getNamespaceURI(ns);
String prefix = reader.getNamespacePrefix(ns);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* 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;

/**
* The maxElements/maxAttributes budgets used to fire only on the wsp:*
* operator walk: every node inside an assertion subtree was materialized into
* DOM by the converter layer with no counting at all, so millions of elements
* could be allocated while the counters reported single digits. These tests
* pin the fix: assertion subtrees are charged against the same parse budget.
*/
public class PolicyBuilderAssertionSubtreeBudgetTest extends PolicyTestCase {

@Test
public void testAssertionSubtreeElementsAreCountedAgainstBudget() {
System.setProperty(PolicyBuilder.MAX_ELEMENTS_PROPERTY, "500");
try {
PolicyBuilder builder = new PolicyBuilder();
try {
builder.getPolicy(xmlStream(buildWideAssertionPolicyXml(600, 0)));
fail("Expected RuntimeException due to element budget");
} catch (RuntimeException ex) {
assertTrue(ex.getMessage().contains("maximum number of elements"));
}
} finally {
System.clearProperty(PolicyBuilder.MAX_ELEMENTS_PROPERTY);
}
}

@Test
public void testAssertionSubtreeAttributesAreCountedAgainstBudget() {
System.setProperty(PolicyBuilder.MAX_ATTRIBUTES_PROPERTY, "500");
try {
PolicyBuilder builder = new PolicyBuilder();
try {
builder.getPolicy(xmlStream(buildWideAssertionPolicyXml(200, 5)));
fail("Expected RuntimeException due to attribute budget");
} catch (RuntimeException ex) {
assertTrue(ex.getMessage().contains("maximum number of attributes"));
}
} finally {
System.clearProperty(PolicyBuilder.MAX_ATTRIBUTES_PROPERTY);
}
}

@Test
public void testSmallAssertionSubtreeParses() {
PolicyBuilder builder = new PolicyBuilder();
Policy policy = builder.getPolicy(xmlStream(buildWideAssertionPolicyXml(10, 2)));

assertNotNull(policy);
}

private static InputStream xmlStream(String xml) {
return new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
}

private static String buildWideAssertionPolicyXml(int elements, int attributesPerElement) {
StringBuilder xml = new StringBuilder(256 + elements * (24 + attributesPerElement * 12));
xml.append("<wsp:Policy xmlns:wsp=\"http://www.w3.org/ns/ws-policy\" xmlns:x=\"urn:x\">")
.append("<x:r>");

for (int i = 0; i < elements; i++) {
xml.append("<x:e");
for (int a = 0; a < attributesPerElement; a++) {
xml.append(" a").append(a).append("=\"v\"");
}
xml.append("/>");
}

xml.append("</x:r>")
.append("</wsp:Policy>");
return xml.toString();
}
}