Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions src/main/java/org/apache/neethi/PolicyReference.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
Expand Down Expand Up @@ -179,6 +180,10 @@ public Policy getRemoteReferencedPolicy(String u) {
// - link-local (169.254.x.x / fe80::/10) — cloud IMDS, auto-configuration
// - multicast (224.0.0.0/4 / ff00::/8) — no HTTP server listens at a multicast address
// - any-local (0.0.0.0 / ::) — unspecified/wildcard, not a valid destination
// - IPv6 unique-local (fd00::/7) - includes cloud IMDS IPv6 endpoints
// (e.g. AWS fd00:ec2::254), the same class the link-local rejection targets
// - IPv4 embedded in IPv6 (64:ff9b::/96 NAT64, ::ffff:0:0/96 mapped) whenever
// the embedded IPv4 address would itself be rejected
// Loopback (127.x.x.x / ::1) and site-local (RFC-1918) addresses are permitted
// so that policies on localhost or an internal network can be resolved.
// EVERY address the host resolves to is vetted, so a multi-record DNS
Expand All @@ -192,7 +197,8 @@ public Policy getRemoteReferencedPolicy(String u) {
for (InetAddress addr : addresses) {
if (isForbiddenAddress(addr)) {
throw new RuntimeException(
"PolicyReference URI resolves to a forbidden address (link-local, multicast, or wildcard).");
"PolicyReference URI resolves to a forbidden address (link-local, multicast,"
+ " wildcard, IPv6 unique-local, or an IPv6 form embedding one).");
}
}

Expand Down Expand Up @@ -287,7 +293,54 @@ static byte[] readBounded(InputStream input, long maxBytes,
* fetcher must never connect to. Package-private for tests.
*/
static boolean isForbiddenAddress(InetAddress addr) {
return addr.isLinkLocalAddress() || addr.isMulticastAddress() || addr.isAnyLocalAddress();
if (addr.isLinkLocalAddress() || addr.isMulticastAddress() || addr.isAnyLocalAddress()) {
return true;
}
if (addr instanceof Inet6Address) {
byte[] bytes = addr.getAddress();
// IPv6 unique-local (fd00::/7, RFC 4193): none of the JDK
// predicates above match it, yet it includes cloud metadata
// endpoints such as the AWS IMDS IPv6 endpoint fd00:ec2::254 -
// exactly the class the link-local rejection exists to block.
if ((bytes[0] & 0xfe) == 0xfc) {
return true;
}
// IPv4 embedded in IPv6 (NAT64 well-known prefix 64:ff9b::/96,
// RFC 6052, and IPv4-mapped ::ffff:0:0/96): classify by the
// embedded IPv4 address the gateway would deliver to.
InetAddress embedded = extractEmbeddedIpv4(bytes);
if (embedded != null && isForbiddenAddress(embedded)) {
return true;
}
}
return false;
}

private static InetAddress extractEmbeddedIpv4(byte[] bytes) {
if (bytes.length != 16) {
return null;
}
boolean nat64 = bytes[0] == 0x00 && bytes[1] == 0x64
&& bytes[2] == (byte) 0xff && bytes[3] == (byte) 0x9b;
for (int i = 4; nat64 && i < 12; i++) {
nat64 = bytes[i] == 0x00;
}
boolean mapped = true;
for (int i = 0; mapped && i < 10; i++) {
mapped = bytes[i] == 0x00;
}
mapped = mapped && bytes[10] == (byte) 0xff && bytes[11] == (byte) 0xff;
if (!nat64 && !mapped) {
return null;
}
try {
return InetAddress.getByAddress(
new byte[] {bytes[12], bytes[13], bytes[14], bytes[15]});
} catch (UnknownHostException e) {
// cannot happen for a 4-byte address; fail closed if it ever does
throw new RuntimeException(
"PolicyReference URI resolves to an address that could not be classified.");
}
}

private static String toUrlHost(InetAddress addr) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package org.apache.neethi;

import java.net.Inet6Address;
import java.net.InetAddress;

import org.junit.Test;
Expand Down Expand Up @@ -68,4 +69,50 @@ public void testIpv4MappedEncodingOfLinkLocalIsForbidden() throws Exception {
assertTrue(PolicyReference.isForbiddenAddress(
InetAddress.getByName("::ffff:169.254.169.254")));
}

@Test
public void testUniqueLocalIsForbidden() throws Exception {
assertTrue(PolicyReference.isForbiddenAddress(InetAddress.getByName("fd00:ec2::254")));
assertTrue(PolicyReference.isForbiddenAddress(InetAddress.getByName("fc00::1")));
assertTrue(PolicyReference.isForbiddenAddress(InetAddress.getByName("fdff::1")));
}

@Test
public void testNat64EmbeddedForbiddenIpv4IsForbidden() throws Exception {
assertTrue(PolicyReference.isForbiddenAddress(
InetAddress.getByName("64:ff9b::169.254.169.254")));
assertTrue(PolicyReference.isForbiddenAddress(
InetAddress.getByName("64:ff9b::0.0.0.0")));
}

@Test
public void testNat64EmbeddedPermittedIpv4StaysPermitted() throws Exception {
// NAT64 form of a public IPv4 address — permitted, like the address itself
assertFalse(PolicyReference.isForbiddenAddress(
InetAddress.getByName("64:ff9b::93.184.216.34")));
// NAT64 form of RFC-1918 — permitted by documented intent (Q6)
assertFalse(PolicyReference.isForbiddenAddress(
InetAddress.getByName("64:ff9b::10.0.0.5")));
}

@Test
public void testMappedFormKeptAsInet6IsClassifiedByEmbeddedAddress() throws Exception {
// getByName normalizes mapped forms to Inet4Address; guard the raw
// Inet6Address representation as well (e.g. from a custom resolver)
byte[] mappedLinkLocal = new byte[16];
mappedLinkLocal[10] = (byte) 0xff;
mappedLinkLocal[11] = (byte) 0xff;
mappedLinkLocal[12] = (byte) 169;
mappedLinkLocal[13] = (byte) 254;
mappedLinkLocal[14] = (byte) 169;
mappedLinkLocal[15] = (byte) 254;

assertTrue(PolicyReference.isForbiddenAddress(
Inet6Address.getByAddress(null, mappedLinkLocal, 0)));
}

@Test
public void testGlobalIpv6StaysPermitted() throws Exception {
assertFalse(PolicyReference.isForbiddenAddress(InetAddress.getByName("2001:db8::1")));
}
}