This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/remote-bytes in repository https://gitbox.apache.org/repos/asf/ws-neethi.git
commit 8c9f0ee121ead5f6d1f3167af34399d0ac2398e3 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Fri Jul 10 11:17:58 2026 +0100 Enforce the amount of bytes that can be read from the remote reference --- README.txt | 26 ++++ src/main/java/org/apache/neethi/PolicyBuilder.java | 6 +- .../java/org/apache/neethi/PolicyReference.java | 58 ++++++- .../org/apache/neethi/PolicyReferenceTest.java | 166 +++++++++++++++++++++ 4 files changed, 252 insertions(+), 4 deletions(-) diff --git a/README.txt b/README.txt index c728efe..fbe66f8 100644 --- a/README.txt +++ b/README.txt @@ -9,3 +9,29 @@ runtime and an extension model for serailization and de-serialization of domain specific Assertions. Please visit : https://ws.apache.org/neethi/ for further infomation. + +Security + +Neethi enforces parser and normalization budgets to reduce the risk of +algorithmic-complexity and resource-exhaustion attacks when processing +untrusted policy documents. + +The following system properties can be used to tune the parser limits. If a +property is unset, blank, zero, negative, or otherwise invalid, Neethi falls +back to the default shown below. + +- `org.apache.neethi.parser.maxDepth` - maximum policy nesting depth. + Default: `256`. +- `org.apache.neethi.parser.maxElements` - maximum number of parsed elements. + Default: `100000`. +- `org.apache.neethi.parser.maxAttributes` - maximum number of parsed + attributes. + Default: `10000`. +- `org.apache.neethi.remote.maxPolicyBytes` - maximum size of a remotely + referenced policy document fetched through `PolicyReference`. + Default: `67108864` bytes (`64 MiB`). + +Policy normalization also enforces a hard cap of `10000` policy alternatives. +This limit applies to the number of normalized alternatives produced by policy +normalization and intersection, and helps prevent crafted policies from +triggering exponential expansion. diff --git a/src/main/java/org/apache/neethi/PolicyBuilder.java b/src/main/java/org/apache/neethi/PolicyBuilder.java index c5f81d0..7941c7c 100644 --- a/src/main/java/org/apache/neethi/PolicyBuilder.java +++ b/src/main/java/org/apache/neethi/PolicyBuilder.java @@ -39,9 +39,9 @@ import org.apache.neethi.builders.AssertionBuilder; */ public class PolicyBuilder { - private static final String MAX_DEPTH_PROPERTY = "org.apache.neethi.parser.maxDepth"; - private static final String MAX_ELEMENTS_PROPERTY = "org.apache.neethi.parser.maxElements"; - private static final String MAX_ATTRIBUTES_PROPERTY = "org.apache.neethi.parser.maxAttributes"; + public static final String MAX_DEPTH_PROPERTY = "org.apache.neethi.parser.maxDepth"; + public static final String MAX_ELEMENTS_PROPERTY = "org.apache.neethi.parser.maxElements"; + public static final String MAX_ATTRIBUTES_PROPERTY = "org.apache.neethi.parser.maxAttributes"; private static final int DEFAULT_MAX_DEPTH = 256; private static final int DEFAULT_MAX_ELEMENTS = 100000; diff --git a/src/main/java/org/apache/neethi/PolicyReference.java b/src/main/java/org/apache/neethi/PolicyReference.java index 80c113d..4de9164 100644 --- a/src/main/java/org/apache/neethi/PolicyReference.java +++ b/src/main/java/org/apache/neethi/PolicyReference.java @@ -19,6 +19,8 @@ package org.apache.neethi; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; @@ -36,14 +38,22 @@ import javax.xml.stream.XMLStreamWriter; */ public class PolicyReference implements PolicyComponent { + public static final String MAX_REMOTE_POLICY_BYTES_PROPERTY = "org.apache.neethi.remote.maxPolicyBytes"; + private static final long DEFAULT_MAX_REMOTE_POLICY_BYTES = 64L * 1024L * 1024L; + private String uri; private PolicyBuilder engine; + private final long maxRemotePolicyBytes; public PolicyReference() { + maxRemotePolicyBytes = readConfiguredLimit(MAX_REMOTE_POLICY_BYTES_PROPERTY, + DEFAULT_MAX_REMOTE_POLICY_BYTES); } public PolicyReference(PolicyBuilder p) { engine = p; + maxRemotePolicyBytes = readConfiguredLimit(MAX_REMOTE_POLICY_BYTES_PROPERTY, + DEFAULT_MAX_REMOTE_POLICY_BYTES); } /** @@ -174,13 +184,21 @@ public class PolicyReference implements PolicyComponent { connection.setReadTimeout(10000); ((HttpURLConnection) connection).setInstanceFollowRedirects(false); + long declaredLength = connection.getContentLengthLong(); + if (declaredLength > maxRemotePolicyBytes) { + throw new RuntimeException( + "Remote policy response exceeded the maximum remote policy size (" + + maxRemotePolicyBytes + " bytes)."); + } + InputStream in = connection.getInputStream(); try { + byte[] payload = readBounded(in, maxRemotePolicyBytes); PolicyBuilder pe = engine; if (pe == null) { pe = new PolicyBuilder(); } - return pe.getPolicy(in); + return pe.getPolicy(new ByteArrayInputStream(payload)); } finally { in.close(); } @@ -188,4 +206,42 @@ public class PolicyReference implements PolicyComponent { throw new RuntimeException("Cannot reach remote policy reference."); } } + + private static byte[] readBounded(InputStream input, long maxBytes) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + long total = 0; + + while (true) { + int read = input.read(buffer); + if (read == -1) { + break; + } + + total += read; + if (total > maxBytes) { + throw new RuntimeException( + "Remote policy response exceeded the maximum remote policy size (" + + maxBytes + " bytes)."); + } + + out.write(buffer, 0, read); + } + + return out.toByteArray(); + } + + private static long readConfiguredLimit(String key, long defaultValue) { + String value = System.getProperty(key); + if (value == null || value.trim().length() == 0) { + return defaultValue; + } + try { + long parsed = Long.parseLong(value.trim()); + return parsed > 0 ? parsed : defaultValue; + } catch (NumberFormatException ex) { + return defaultValue; + } + } + } diff --git a/src/test/java/org/apache/neethi/PolicyReferenceTest.java b/src/test/java/org/apache/neethi/PolicyReferenceTest.java index b5b99a8..b1e3557 100644 --- a/src/test/java/org/apache/neethi/PolicyReferenceTest.java +++ b/src/test/java/org/apache/neethi/PolicyReferenceTest.java @@ -19,14 +19,20 @@ package org.apache.neethi; +import java.io.OutputStream; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import static org.apache.neethi.PolicyReference.MAX_REMOTE_POLICY_BYTES_PROPERTY; + import org.junit.Test; /** @@ -243,4 +249,164 @@ public class PolicyReferenceTest extends PolicyTestCase { e.getMessage() != null && e.getMessage().toLowerCase().contains("circular")); } } + + @Test + public void testRemotePolicyDeclaredContentLengthAboveLimitIsRejected() throws Exception { + String previous = System.getProperty(MAX_REMOTE_POLICY_BYTES_PROPERTY); + System.setProperty(MAX_REMOTE_POLICY_BYTES_PROPERTY, "1024"); + + byte[] payload = buildLargePolicyPayload(2048); + try (LocalHttpServer server = LocalHttpServer.fixedLength(payload)) { + String url = "http://127.0.0.1:" + server.getPort() + "/policy.xml"; + PolicyReference ref = new PolicyReference(policyEngine); + try { + ref.getRemoteReferencedPolicy(url); + fail("Expected RuntimeException due to remote response byte budget"); + } catch (RuntimeException ex) { + assertTrue(ex.getMessage().contains("maximum remote policy size")); + } + } finally { + restoreProperty(MAX_REMOTE_POLICY_BYTES_PROPERTY, previous); + } + } + + @Test + public void testRemotePolicyChunkedResponseAboveLimitIsRejected() throws Exception { + String previous = System.getProperty(MAX_REMOTE_POLICY_BYTES_PROPERTY); + System.setProperty(MAX_REMOTE_POLICY_BYTES_PROPERTY, "1024"); + + byte[] payload = buildLargePolicyPayload(4096); + try (LocalHttpServer server = LocalHttpServer.chunked(payload)) { + String url = "http://127.0.0.1:" + server.getPort() + "/policy.xml"; + PolicyReference ref = new PolicyReference(policyEngine); + try { + ref.getRemoteReferencedPolicy(url); + fail("Expected RuntimeException due to remote response byte budget"); + } catch (RuntimeException ex) { + assertTrue(ex.getMessage().contains("maximum remote policy size")); + } + } finally { + restoreProperty(MAX_REMOTE_POLICY_BYTES_PROPERTY, previous); + } + } + + private static void restoreProperty(String key, String previous) { + if (previous == null) { + System.clearProperty(key); + } else { + System.setProperty(key, previous); + } + } + + private static byte[] buildLargePolicyPayload(int totalBytes) { + String prefix = "<wsp:Policy xmlns:wsp=\"http://www.w3.org/ns/ws-policy\">"; + String suffix = "</wsp:Policy>"; + int middleSize = Math.max(0, totalBytes - prefix.length() - suffix.length()); + char[] middle = new char[middleSize]; + Arrays.fill(middle, ' '); + String xml = prefix + new String(middle) + suffix; + return xml.getBytes(StandardCharsets.UTF_8); + } + + private static final class LocalHttpServer implements AutoCloseable { + private final ServerSocket serverSocket; + private final Thread serverThread; + private final byte[] payload; + private final boolean sendContentLength; + private final CountDownLatch ready = new CountDownLatch(1); + private final AtomicBoolean closed = new AtomicBoolean(false); + + static LocalHttpServer fixedLength(byte[] payload) throws IOException { + return new LocalHttpServer(payload, true); + } + + static LocalHttpServer chunked(byte[] payload) throws IOException { + return new LocalHttpServer(payload, false); + } + + LocalHttpServer(byte[] payload, boolean sendContentLength) throws IOException { + this.payload = payload; + this.sendContentLength = sendContentLength; + this.serverSocket = new ServerSocket(0); + this.serverThread = new Thread(this::serveOnce, "policy-reference-test-server"); + this.serverThread.setDaemon(true); + this.serverThread.start(); + awaitReady(); + } + + int getPort() { + return serverSocket.getLocalPort(); + } + + @Override + public void close() throws IOException { + if (closed.compareAndSet(false, true)) { + serverSocket.close(); + try { + serverThread.join(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + private void awaitReady() throws IOException { + try { + if (!ready.await(1, TimeUnit.SECONDS)) { + throw new IOException("Test HTTP server did not start in time"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting for test HTTP server", e); + } + } + + private void serveOnce() { + ready.countDown(); + try (Socket client = serverSocket.accept()) { + consumeRequest(client.getInputStream()); + writeResponse(client.getOutputStream()); + } catch (IOException ignored) { + // The tests close the server socket during cleanup. + } + } + + private void consumeRequest(InputStream input) throws IOException { + byte[] buffer = new byte[1024]; + int matched = 0; + while (matched < 4) { + int read = input.read(buffer); + if (read == -1) { + break; + } + for (int i = 0; i < read; i++) { + byte b = buffer[i]; + if ((matched == 0 || matched == 2) && b == '\r') { + matched++; + } else if ((matched == 1 || matched == 3) && b == '\n') { + matched++; + } else { + matched = 0; + } + if (matched == 4) { + return; + } + } + } + } + + private void writeResponse(OutputStream out) throws IOException { + StringBuilder response = new StringBuilder(); + response.append("HTTP/1.1 200 OK\r\n"); + response.append("Content-Type: application/xml\r\n"); + response.append("Connection: close\r\n"); + if (sendContentLength) { + response.append("Content-Length: ").append(payload.length).append("\r\n"); + } + response.append("\r\n"); + out.write(response.toString().getBytes(StandardCharsets.US_ASCII)); + out.write(payload); + out.flush(); + } + } }
