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();
+        }
+    }
 }

Reply via email to