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.