Skip to content

Commit 2f4e2d3

Browse files
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 <noreply@anthropic.com>
1 parent dccf153 commit 2f4e2d3

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

SECURITY.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,12 @@ migration from `commons-fileupload` 1.x to `commons-fileupload2` in
268268
`blockPrivateNetworkResponseEndpoints` additionally refuses loopback and
269269
private ranges; it is off by default because a callback inside the same
270270
private network is how most decoupled deployments are wired.
271+
- The address checks are address-family agnostic: a bracketed IPv6 literal
272+
is classified, not treated as an unrecognised host, and the IPv4-mapped
273+
form (`[::ffff:169.254.169.254]`) is refused as the address it reaches
274+
rather than as a separate spelling. IPv6 unique-local (`fc00::/7`) is
275+
covered explicitly, since `InetAddress.isSiteLocalAddress` answers only
276+
for the deprecated `fec0::/10`.
271277
- Redirects are not followed, so a reply endpoint cannot hand the sender a
272278
destination the policy already refused.
273279
- Name resolution is bounded (`responseEndpointResolveTimeoutMillis`) and
@@ -289,6 +295,15 @@ migration from `commons-fileupload` 1.x to `commons-fileupload2` in
289295
parts as soon as their text is read, file parts once the item backing the
290296
`DataHandler` is unreachable.
291297

298+
Both ceilings are enforced against bytes actually read, not against a
299+
declared `Content-Length`, so a chunked request body is bounded on the same
300+
terms as a declared one. This is worth stating because the reverse is the
301+
easy mistake: a limit that screens the header before the read is no limit
302+
at all for `Transfer-Encoding: chunked`, which declares no length. The
303+
form-urlencoded builder wraps the stream in `BoundedInputStream`; the
304+
multipart path relies on commons-fileupload2, which pairs its
305+
`Content-Length` fast path with a streaming guard.
306+
292307
11. **OpenAPI and Swagger UI output (2.0.2):** Request-controlled values are
293308
validated and encoded for the context they are written into, the served
294309
page carries a Content-Security-Policy with a per-response script nonce,

modules/kernel/test/org/apache/axis2/addressing/AddressingResponseEndpointPolicyTest.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,81 @@ public void testWildcardAndMulticastAlwaysBlocked() {
147147
new EndpointReference("http://239.1.2.3/sink"), messageContext));
148148
}
149149

150+
/**
151+
* IPv6 destinations are classified, not waved through.
152+
*
153+
* <p>Nothing here needed a code change — {@code URI.getHost} keeps the
154+
* brackets, {@code InetAddress.getByName} accepts that form, and the
155+
* {@code isLinkLocalAddress}/{@code isAnyLocalAddress}/
156+
* {@code isMulticastAddress} family is address-family agnostic. The suite
157+
* had no IPv6 case at all, though, so the behaviour was correct and
158+
* unverified, and a regression here would be silent. The equivalent checks
159+
* in Axis2/C had to be written by hand and were wrong until they were.
160+
*/
161+
public void testIPv6DestinationsAreClassified() {
162+
// Refused whatever the configuration says.
163+
assertFalse("link-local", AddressingResponseEndpointPolicy.isAllowed(
164+
new EndpointReference("http://[fe80::1]/sink"), messageContext));
165+
assertFalse("link-local with a port", AddressingResponseEndpointPolicy.isAllowed(
166+
new EndpointReference("http://[fe80::1]:8080/sink"), messageContext));
167+
assertFalse("unspecified", AddressingResponseEndpointPolicy.isAllowed(
168+
new EndpointReference("http://[::]/sink"), messageContext));
169+
assertFalse("multicast", AddressingResponseEndpointPolicy.isAllowed(
170+
new EndpointReference("http://[ff02::1]/sink"), messageContext));
171+
172+
// A global address is a legitimate destination; without this the rest
173+
// would pass just as well if IPv6 were refused wholesale.
174+
assertTrue("a global address must still be allowed",
175+
AddressingResponseEndpointPolicy.isAllowed(
176+
new EndpointReference("http://[2001:db8::1]/sink"), messageContext));
177+
}
178+
179+
/**
180+
* The IPv4-mapped form reaches the same metadata service the dotted quad
181+
* does, so it has to be refused the same way. The JDK resolves
182+
* {@code ::ffff:169.254.169.254} to an {@code Inet4Address}, which is what
183+
* makes this work without a special case — worth pinning, because it is a
184+
* property of the JDK rather than of this code.
185+
*/
186+
public void testIPv4MappedMetadataAddressIsBlocked() {
187+
assertFalse(AddressingResponseEndpointPolicy.isAllowed(
188+
new EndpointReference("http://[::ffff:169.254.169.254]/latest/meta-data/"),
189+
messageContext));
190+
assertFalse(AddressingResponseEndpointPolicy.isAllowed(
191+
new EndpointReference("http://[::ffff:0.0.0.0]/sink"), messageContext));
192+
}
193+
194+
/**
195+
* Loopback and unique-local follow the private-network switch, as their
196+
* IPv4 counterparts do.
197+
*
198+
* <p>{@code fc00::/7} is the one part of this that is not the JDK's doing:
199+
* {@code isSiteLocalAddress} answers for the deprecated {@code fec0::/10}
200+
* and returns false for a unique-local address, so {@code isUniqueLocalIPv6}
201+
* covers it. That is exactly the kind of gap this test exists to hold shut.
202+
*/
203+
public void testIPv6LoopbackAndUniqueLocalFollowThePrivateSwitch() throws Exception {
204+
assertTrue("loopback is allowed until the switch is set",
205+
AddressingResponseEndpointPolicy.isAllowed(
206+
new EndpointReference("http://[::1]/sink"), messageContext));
207+
assertTrue("unique-local is allowed until the switch is set",
208+
AddressingResponseEndpointPolicy.isAllowed(
209+
new EndpointReference("http://[fd00::1]/sink"), messageContext));
210+
211+
setParameter(AddressingResponseEndpointPolicy.BLOCK_PRIVATE_NETWORKS, "true");
212+
213+
assertFalse("::1", AddressingResponseEndpointPolicy.isAllowed(
214+
new EndpointReference("http://[::1]/sink"), messageContext));
215+
assertFalse("fd00::/8 unique-local", AddressingResponseEndpointPolicy.isAllowed(
216+
new EndpointReference("http://[fd00::1]/sink"), messageContext));
217+
assertFalse("fc00::/7 unique-local", AddressingResponseEndpointPolicy.isAllowed(
218+
new EndpointReference("http://[fc00::1]/sink"), messageContext));
219+
assertFalse("fec0::/10 site-local", AddressingResponseEndpointPolicy.isAllowed(
220+
new EndpointReference("http://[fec0::1]/sink"), messageContext));
221+
assertFalse("v4-mapped loopback", AddressingResponseEndpointPolicy.isAllowed(
222+
new EndpointReference("http://[::ffff:127.0.0.1]/sink"), messageContext));
223+
}
224+
150225
/**
151226
* The schemes that only ever serve as an SSRF pivot are refused before any
152227
* host check.

0 commit comments

Comments
 (0)