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
3 changes: 3 additions & 0 deletions README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ back to the default shown below.
- `org.apache.neethi.remote.maxPolicyBytes` - maximum size of a remotely
referenced policy document fetched through `PolicyReference`.
Default: `67108864` bytes (`64 MiB`).
- `org.apache.neethi.remote.maxFetchMillis` - maximum total time allowed to
fetch a remote policy document through `PolicyReference`.
Default: `30000` milliseconds (`30 seconds`).

Policy normalization also enforces several hard caps:

Expand Down
31 changes: 20 additions & 11 deletions THREAT-MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,12 @@ A finding is in-model only if it reaches a row marked **yes**.
- **Network**: `PolicyReference.getRemoteReferencedPolicy` opens an
HTTP/HTTPS connection via the JDK URL handler with documented
timeouts (`connectTimeout=5000`, `readTimeout=10000`,
`setInstanceFollowRedirects(false)`) *(documented:
`PolicyReference.java` lines 170-175)*.
`setInstanceFollowRedirects(false)`) and a configurable total fetch
deadline (`org.apache.neethi.remote.maxFetchMillis`, default `30000` ms)
*(documented: `PolicyReference.java` and `README.txt`)*. The read timeout
is an inactivity timeout for an individual read; the total deadline is
checked between reads, so a blocking read can extend the practical bound
by up to the read timeout.
- **DNS**: the remote-policy fetcher calls `InetAddress.getByName(host)`
to perform the address-class check *(documented:
`PolicyReference.java` line 161)*. Compromised DNS can spoof the
Expand Down Expand Up @@ -230,6 +234,7 @@ feature toggles**. The runtime security envelope is shaped by:
| `PolicyReference.getRemoteReferencedPolicy` — address-class filter | rejects link-local, multicast, any-local; **permits** loopback, RFC-1918 / site-local, public *(documented: `PolicyReference.java` lines 149-168)* | **maintainer ruling required** — see §14 Q6 | controls whether SSRF against loopback (e.g. localhost IMDS proxy) or RFC-1918 (internal microservices) is possible |
| `PolicyReference.getRemoteReferencedPolicy` — connect timeout | `5000` ms *(documented: `PolicyReference.java` line 173)* | hardened-by-default | bound on per-fetch connect time |
| `PolicyReference.getRemoteReferencedPolicy` — read timeout | `10000` ms *(documented: `PolicyReference.java` line 174)* | hardened-by-default | bound on per-fetch read time |
| `org.apache.neethi.remote.maxFetchMillis` | `30000` ms (`30 s`) *(documented: `README.txt`)* | hardened-by-default | bounds total remote-policy response-fetch time; checked between reads |
| `PolicyReference.getRemoteReferencedPolicy` — redirects | follow disabled (`setInstanceFollowRedirects(false)`) *(documented: `PolicyReference.java` line 175)* | hardened-by-default | prevents redirect-based filter bypass |
| `PolicyReference.getRemoteReferencedPolicy` — supported schemes | `http`, `https` only — others rejected *(documented: `PolicyReference.java` lines 149-152)* | hardened-by-default | blocks `file:`, `jar:`, `ftp:`, etc. |
| `org.apache.neethi.parser.maxDepth` | `256` *(documented: `README.txt`)* | hardened-by-default | bounds maximum parser nesting depth |
Expand Down Expand Up @@ -311,9 +316,12 @@ overload disables both. The pre-parsed-`Element` / `XMLStreamReader` /
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)*.
- Connect-timeout (5 s), read inactivity-timeout (10 s), and the default
total fetch deadline (30 s) bound each direct fetch. Because the total
deadline is checked between reads, a blocking read can extend the
practical bound by up to the read timeout. An embedder that directly
dereferences many distinct unresolved references can multiply the latency
*(inferred — §14 Q7)*.

## §7 Adversary model

Expand Down Expand Up @@ -434,16 +442,17 @@ overload disables both. The pre-parsed-`Element` / `XMLStreamReader` /
- *(documented: `PolicyReference.java` line 175 —
`setInstanceFollowRedirects(false)`)*

### P10 — Per-fetch connect-timeout and read-timeout on remote `PolicyReference` resolution
### P10 — Total and per-read timing bounds on remote `PolicyReference` resolution

- **Condition**: same as P7; the named server hangs or stalls.
- **Violation symptom**: `PolicyReference.normalize(...)` or
`getRemoteReferencedPolicy(...)` blocks for more than ~15 seconds
per reference.
`getRemoteReferencedPolicy(...)` remains blocked beyond the configured
total fetch deadline plus the maximum individual read timeout, or a
silent connection is not rejected by the connect/read timeouts.
- **Severity**: **availability-relevant**; `VALID-HARDENING`
*(inferred — §14 Q7)* — wall-clock bound but no bound on the
*number* of references an embedder dereferences directly.
- *(documented: `PolicyReference.java` lines 173-174)*
- *(documented: `PolicyReference.java`, `README.txt`)*

### P11 — Policy intersection / equivalence is total: any two well-formed `Policy` objects can be compared

Expand Down Expand Up @@ -846,15 +855,15 @@ source comments. The project website is
| Source | Claim | Lands in |
| --- | --- | --- |
| `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`, normalization/intersection output cap `10000` alternatives, and `PolicyIntersector` work budget `MAX_INTERSECT_STEPS=1000000`; invalid/unset values fall back to defaults | §5a, §6, §8 P2-P6/P13, §10 item 4 |
| `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`, `org.apache.neethi.remote.maxFetchMillis=30000`, normalization/intersection output cap `10000` alternatives, and `PolicyIntersector` work budget `MAX_INTERSECT_STEPS=1000000`; invalid/unset values fall back to defaults | §5a, §6, §8 P2-P6/P10/P13, §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 |
| `PolicyReference.java` lines 149-152 | "Unsupported URI scheme: only http and https are permitted" | §8 P8, §11a |
| `PolicyReference.java` lines 154-159 | "Resolve the host to an IP and reject addresses that can never serve a policy document: link-local … multicast … any-local … Loopback (127.x.x.x / ::1) and site-local (RFC-1918) addresses are permitted so that policies on localhost or an internal network can be resolved" | §5a (insecure-default case), §8 P7, §9, §14 Q6 |
| `PolicyReference.java` lines 160-168 | `InetAddress.getByName(...)` address-class check | §8 P7, §11a, §14 Q8 |
| `PolicyReference.java` lines 173-175 | `connectTimeout=5000`, `readTimeout=10000`, `setInstanceFollowRedirects(false)` | §5a, §8 P9-P10 |
| `PolicyReference.java` lines 173-175 | `connectTimeout=5000`, `readTimeout=10000`, `setInstanceFollowRedirects(false)`, plus the total fetch deadline configured by `org.apache.neethi.remote.maxFetchMillis` | §5a, §8 P9-P10 |
| `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 |
Expand Down
23 changes: 21 additions & 2 deletions src/main/java/org/apache/neethi/PolicyReference.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,28 @@
public class PolicyReference implements PolicyComponent {

public static final String MAX_REMOTE_POLICY_BYTES_PROPERTY = "org.apache.neethi.remote.maxPolicyBytes";
public static final String MAX_REMOTE_FETCH_MILLIS_PROPERTY = "org.apache.neethi.remote.maxFetchMillis";
private static final long DEFAULT_MAX_REMOTE_POLICY_BYTES = 64L * 1024L * 1024L;
private static final long DEFAULT_MAX_REMOTE_FETCH_MILLIS = 30L * 1000L;

private String uri;
private PolicyBuilder engine;
private final long maxRemotePolicyBytes;
private final long maxRemoteFetchMillis;

public PolicyReference() {
maxRemotePolicyBytes = readConfiguredLimit(MAX_REMOTE_POLICY_BYTES_PROPERTY,
DEFAULT_MAX_REMOTE_POLICY_BYTES);
maxRemoteFetchMillis = readConfiguredLimit(MAX_REMOTE_FETCH_MILLIS_PROPERTY,
DEFAULT_MAX_REMOTE_FETCH_MILLIS);
}

public PolicyReference(PolicyBuilder p) {
engine = p;
maxRemotePolicyBytes = readConfiguredLimit(MAX_REMOTE_POLICY_BYTES_PROPERTY,
DEFAULT_MAX_REMOTE_POLICY_BYTES);
maxRemoteFetchMillis = readConfiguredLimit(MAX_REMOTE_FETCH_MILLIS_PROPERTY,
DEFAULT_MAX_REMOTE_FETCH_MILLIS);
}

/**
Expand Down Expand Up @@ -191,9 +198,14 @@ public Policy getRemoteReferencedPolicy(String u) {
+ maxRemotePolicyBytes + " bytes).");
}

// total wall-clock deadline for the whole fetch: the JDK read
// timeout below applies to each blocking read individually, so a
// server trickling one byte per interval would otherwise hold the
// resolving thread forever
long deadlineNanos = System.nanoTime() + maxRemoteFetchMillis * 1_000_000L;
InputStream in = connection.getInputStream();
try {
byte[] payload = readBounded(in, maxRemotePolicyBytes);
byte[] payload = readBounded(in, maxRemotePolicyBytes, deadlineNanos, maxRemoteFetchMillis);
PolicyBuilder pe = engine;
if (pe == null) {
pe = new PolicyBuilder();
Expand All @@ -207,12 +219,19 @@ public Policy getRemoteReferencedPolicy(String u) {
}
}

private static byte[] readBounded(InputStream input, long maxBytes) throws IOException {
// package-private for tests
static byte[] readBounded(InputStream input, long maxBytes,
long deadlineNanos, long maxMillis) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
long total = 0;

while (true) {
if (System.nanoTime() - deadlineNanos >= 0) {
throw new RuntimeException(
"Remote policy fetch exceeded the maximum total fetch time ("
+ maxMillis + " ms).");
}
int read = input.read(buffer);
if (read == -1) {
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* 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.IOException;
import java.io.InputStream;

import org.junit.Test;

/**
* The JDK read timeout applies to each blocking read individually, so a
* trickle server (one byte per interval, forever) never trips it and never
* reaches EOF or the byte cap: the resolving thread used to block without
* bound. readBounded must enforce a total wall-clock deadline per fetch.
*/
public class PolicyReferenceFetchDeadlineTest extends PolicyTestCase {

@Test
public void testExpiredDeadlineAbortsBeforeReading() throws IOException {
InputStream neverEnding = new ByteArrayInputStream(new byte[1024]);
long expiredDeadline = System.nanoTime() - 1;

try {
PolicyReference.readBounded(neverEnding, 1024L * 1024L, expiredDeadline, 0);
fail("Expected RuntimeException due to total fetch deadline");
} catch (RuntimeException ex) {
assertTrue(ex.getMessage().contains("maximum total fetch time"));
}
}

@Test
public void testTrickleStreamIsCutOffAtDeadline() throws IOException {
InputStream trickle = new InputStream() {
@Override
public int read() throws IOException {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
}
return 'x';
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
b[off] = (byte) read();
return 1;
}
};
long deadline = System.nanoTime() + 100L * 1_000_000L;

try {
PolicyReference.readBounded(trickle, Long.MAX_VALUE, deadline, 100);
fail("Expected RuntimeException due to total fetch deadline");
} catch (RuntimeException ex) {
assertTrue(ex.getMessage().contains("maximum total fetch time"));
}
}

@Test
public void testFastStreamWithinDeadlineIsReadFully() throws IOException {
byte[] payload = "policy-bytes".getBytes("UTF-8");
long deadline = System.nanoTime() + 10_000L * 1_000_000L;

byte[] read = PolicyReference.readBounded(
new ByteArrayInputStream(payload), 1024L, deadline, 10_000);

assertEquals(payload.length, read.length);
}
}