This is an automated email from the ASF dual-hosted git repository.

robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git


The following commit(s) were added to refs/heads/master by this push:
     new 2f4e2d3e5e Cover IPv6 response endpoints and record how the body 
ceilings measure
2f4e2d3e5e is described below

commit 2f4e2d3e5ea2a6868a6711be0b6c327d652e643f
Author: Robert Lazarski <[email protected]>
AuthorDate: Tue Aug 11 07:41:59 2026 -1000

    Cover IPv6 response endpoints and record how the body ceilings measure
    
    Neither gap that had to be fixed in the C implementation exists here, but 
for
    reasons worth writing down rather than rediscovering.
    
    The body ceilings measure bytes actually read, not a declared 
Content-Length,
    so a chunked body is bounded on the same terms as a declared one. The
    form-urlencoded builder wraps the stream; the multipart path relies on
    commons-fileupload2, which pairs its Content-Length fast path with a 
streaming
    guard. SECURITY.md now says so, because the reverse is the easy mistake to 
make
    when adding a limit: screening the header before the read leaves
    Transfer-Encoding: chunked unbounded, which declares no length.
    
    Address classification is address-family agnostic because it defers to
    InetAddress rather than parsing hosts, and URI.getHost keeps the brackets 
in a
    form InetAddress accepts. The one part not inherited is fc00::/7, since
    isSiteLocalAddress answers only for the deprecated fec0::/10; 
isUniqueLocalIPv6
    already covered it.
    
    What was missing was any test at all: seventeen cases, none of them IPv6, so
    the behaviour was right and unverified and a regression would have been 
silent.
    Three tests now cover the always-refused set, the IPv4-mapped metadata 
address,
    and loopback and unique-local under the private-network switch, plus a 
global
    address as the control so the suite cannot pass by refusing IPv6 wholesale.
    Removing the isUniqueLocalIPv6 clause fails exactly one of them; 20 pass 
with
    it in place.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
---
 SECURITY.md                                        | 15 +++++
 .../AddressingResponseEndpointPolicyTest.java      | 75 ++++++++++++++++++++++
 2 files changed, 90 insertions(+)

diff --git a/SECURITY.md b/SECURITY.md
index ed4b7fdf6f..6b94acc16a 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -268,6 +268,12 @@ migration from `commons-fileupload` 1.x to 
`commons-fileupload2` in
      `blockPrivateNetworkResponseEndpoints` additionally refuses loopback and
      private ranges; it is off by default because a callback inside the same
      private network is how most decoupled deployments are wired.
+   - The address checks are address-family agnostic: a bracketed IPv6 literal
+     is classified, not treated as an unrecognised host, and the IPv4-mapped
+     form (`[::ffff:169.254.169.254]`) is refused as the address it reaches
+     rather than as a separate spelling. IPv6 unique-local (`fc00::/7`) is
+     covered explicitly, since `InetAddress.isSiteLocalAddress` answers only
+     for the deprecated `fec0::/10`.
    - Redirects are not followed, so a reply endpoint cannot hand the sender a
      destination the policy already refused.
    - Name resolution is bounded (`responseEndpointResolveTimeoutMillis`) and
@@ -289,6 +295,15 @@ migration from `commons-fileupload` 1.x to 
`commons-fileupload2` in
     parts as soon as their text is read, file parts once the item backing the
     `DataHandler` is unreachable.
 
+    Both ceilings are enforced against bytes actually read, not against a
+    declared `Content-Length`, so a chunked request body is bounded on the same
+    terms as a declared one. This is worth stating because the reverse is the
+    easy mistake: a limit that screens the header before the read is no limit
+    at all for `Transfer-Encoding: chunked`, which declares no length. The
+    form-urlencoded builder wraps the stream in `BoundedInputStream`; the
+    multipart path relies on commons-fileupload2, which pairs its
+    `Content-Length` fast path with a streaming guard.
+
 11. **OpenAPI and Swagger UI output (2.0.2):** Request-controlled values are
     validated and encoded for the context they are written into, the served
     page carries a Content-Security-Policy with a per-response script nonce,
diff --git 
a/modules/kernel/test/org/apache/axis2/addressing/AddressingResponseEndpointPolicyTest.java
 
b/modules/kernel/test/org/apache/axis2/addressing/AddressingResponseEndpointPolicyTest.java
index 67522a170a..8e3773c6ce 100644
--- 
a/modules/kernel/test/org/apache/axis2/addressing/AddressingResponseEndpointPolicyTest.java
+++ 
b/modules/kernel/test/org/apache/axis2/addressing/AddressingResponseEndpointPolicyTest.java
@@ -147,6 +147,81 @@ public class AddressingResponseEndpointPolicyTest extends 
TestCase {
                 new EndpointReference("http://239.1.2.3/sink";), 
messageContext));
     }
 
+    /**
+     * IPv6 destinations are classified, not waved through.
+     *
+     * <p>Nothing here needed a code change — {@code URI.getHost} keeps the
+     * brackets, {@code InetAddress.getByName} accepts that form, and the
+     * {@code isLinkLocalAddress}/{@code isAnyLocalAddress}/
+     * {@code isMulticastAddress} family is address-family agnostic. The suite
+     * had no IPv6 case at all, though, so the behaviour was correct and
+     * unverified, and a regression here would be silent. The equivalent checks
+     * in Axis2/C had to be written by hand and were wrong until they were.
+     */
+    public void testIPv6DestinationsAreClassified() {
+        // Refused whatever the configuration says.
+        assertFalse("link-local", AddressingResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://[fe80::1]/sink";), 
messageContext));
+        assertFalse("link-local with a port", 
AddressingResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://[fe80::1]:8080/sink";), 
messageContext));
+        assertFalse("unspecified", AddressingResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://[::]/sink";), messageContext));
+        assertFalse("multicast", AddressingResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://[ff02::1]/sink";), 
messageContext));
+
+        // A global address is a legitimate destination; without this the rest
+        // would pass just as well if IPv6 were refused wholesale.
+        assertTrue("a global address must still be allowed",
+                AddressingResponseEndpointPolicy.isAllowed(
+                        new EndpointReference("http://[2001:db8::1]/sink";), 
messageContext));
+    }
+
+    /**
+     * The IPv4-mapped form reaches the same metadata service the dotted quad
+     * does, so it has to be refused the same way. The JDK resolves
+     * {@code ::ffff:169.254.169.254} to an {@code Inet4Address}, which is what
+     * makes this work without a special case — worth pinning, because it is a
+     * property of the JDK rather than of this code.
+     */
+    public void testIPv4MappedMetadataAddressIsBlocked() {
+        assertFalse(AddressingResponseEndpointPolicy.isAllowed(
+                new 
EndpointReference("http://[::ffff:169.254.169.254]/latest/meta-data/";),
+                messageContext));
+        assertFalse(AddressingResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://[::ffff:0.0.0.0]/sink";), 
messageContext));
+    }
+
+    /**
+     * Loopback and unique-local follow the private-network switch, as their
+     * IPv4 counterparts do.
+     *
+     * <p>{@code fc00::/7} is the one part of this that is not the JDK's doing:
+     * {@code isSiteLocalAddress} answers for the deprecated {@code fec0::/10}
+     * and returns false for a unique-local address, so {@code 
isUniqueLocalIPv6}
+     * covers it. That is exactly the kind of gap this test exists to hold 
shut.
+     */
+    public void testIPv6LoopbackAndUniqueLocalFollowThePrivateSwitch() throws 
Exception {
+        assertTrue("loopback is allowed until the switch is set",
+                AddressingResponseEndpointPolicy.isAllowed(
+                        new EndpointReference("http://[::1]/sink";), 
messageContext));
+        assertTrue("unique-local is allowed until the switch is set",
+                AddressingResponseEndpointPolicy.isAllowed(
+                        new EndpointReference("http://[fd00::1]/sink";), 
messageContext));
+
+        setParameter(AddressingResponseEndpointPolicy.BLOCK_PRIVATE_NETWORKS, 
"true");
+
+        assertFalse("::1", AddressingResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://[::1]/sink";), messageContext));
+        assertFalse("fd00::/8 unique-local", 
AddressingResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://[fd00::1]/sink";), 
messageContext));
+        assertFalse("fc00::/7 unique-local", 
AddressingResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://[fc00::1]/sink";), 
messageContext));
+        assertFalse("fec0::/10 site-local", 
AddressingResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://[fec0::1]/sink";), 
messageContext));
+        assertFalse("v4-mapped loopback", 
AddressingResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://[::ffff:127.0.0.1]/sink";), 
messageContext));
+    }
+
     /**
      * The schemes that only ever serve as an SSRF pivot are refused before any
      * host check.

Reply via email to