Skip to content
92 changes: 84 additions & 8 deletions core/src/main/java/org/stellar/anchor/util/ClientDomainHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,90 @@ private static Dns validatingDns() {
}

private static boolean isNonPublicAddress(InetAddress address) {
return address.isLoopbackAddress()
|| address.isSiteLocalAddress()
|| address.isLinkLocalAddress()
|| address.isAnyLocalAddress()
|| isCarrierGradeNat(address)
|| isIpv6UniqueLocal(address)
|| isIetfProtocolAssignment(address)
|| isBenchmarkingRange(address);
InetAddress unwrapped = unwrapEmbeddedIPv4(address);
return unwrapped.isLoopbackAddress()
|| unwrapped.isSiteLocalAddress()
|| unwrapped.isLinkLocalAddress()
|| unwrapped.isAnyLocalAddress()
|| isCarrierGradeNat(unwrapped)
|| isIpv6UniqueLocal(unwrapped)
|| isIetfProtocolAssignment(unwrapped)
|| isBenchmarkingRange(unwrapped)
|| isThisNetwork(unwrapped)
|| isNat64WellKnown(address)
Comment thread
amandagonsalves marked this conversation as resolved.
|| isNat64LocalUse(address)
|| is6to4(address);
}

private static InetAddress unwrapEmbeddedIPv4(InetAddress address) {
byte[] a = address.getAddress();
if (a.length != 16) {
return address;
}

byte[] embedded;
if (isNat64WellKnown(address)) {
embedded = new byte[] {a[12], a[13], a[14], a[15]};
} else if (is6to4(address)) {
embedded = new byte[] {a[2], a[3], a[4], a[5]};
} else if (isIpv4Compatible(a)) {
embedded = new byte[] {a[12], a[13], a[14], a[15]};
} else {
return address;
}

try {
return InetAddress.getByAddress(embedded);
} catch (UnknownHostException e) {
return address;
}
}

private static boolean isIpv4Compatible(byte[] a) {
for (int i = 0; i < 10; i++) {
if (a[i] != 0) {
return false;
}
}
return a[10] == 0 && a[11] == 0;
}

private static boolean isThisNetwork(InetAddress address) {
byte[] a = address.getAddress();
return a.length == 4 && (a[0] & 0xFF) == 0;
}

private static boolean isNat64WellKnown(InetAddress address) {
byte[] a = address.getAddress();
if (a.length != 16) {
return false;
}
byte[] prefix = {0, 0x64, (byte) 0xff, (byte) 0x9b, 0, 0, 0, 0, 0, 0, 0, 0};
for (int i = 0; i < prefix.length; i++) {
if (a[i] != prefix[i]) {
return false;
}
}
return true;
}

private static boolean isNat64LocalUse(InetAddress address) {
byte[] a = address.getAddress();
if (a.length != 16) {
return false;
}
byte[] prefix = {0, 0x64, (byte) 0xff, (byte) 0x9b, 0, 1};
for (int i = 0; i < prefix.length; i++) {
if (a[i] != prefix[i]) {
return false;
}
}
return true;
}

private static boolean is6to4(InetAddress address) {
byte[] a = address.getAddress();
return a.length == 16 && (a[0] & 0xFF) == 0x20 && (a[1] & 0xFF) == 0x02;
}

private static boolean isCarrierGradeNat(InetAddress address) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.stellar.anchor.util

import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.stellar.anchor.api.exception.SepException

class SsrfBlocklistBypassTest {
private fun guardBlocks(host: String): Boolean =
try {
ClientDomainHelper.validateDomainNotPrivateNetwork(host)
false
} catch (e: SepException) {
e.message?.contains("non-public") == true
}

@Test
fun `CONTROL - standard private and reserved ranges are correctly blocked`() {
for (h in
listOf(
"127.0.0.1",
"10.0.0.1",
"172.16.0.1",
"192.168.1.1",
"169.254.169.254",
"100.64.0.1",
"::1",
"fc00::1",
"fd00::1",
"::ffff:10.0.0.1",
Comment thread
amandagonsalves marked this conversation as resolved.
"::ffff:169.254.169.254",
"::10.0.0.1"
)) {
assertTrue(guardBlocks(h), "$h must be blocked by the private-network guard")
}
}

@Test
fun `BYPASS - NAT64 (64_ff9b__96) addresses embedding internal IPv4 are now blocked`() {
for (h in listOf("64:ff9b::a9fe:a9fe", "64:ff9b::a00:1", "64:ff9b::7f00:1")) {
assertTrue(
guardBlocks(h),
"$h reaches an internal IPv4 through a NAT64 gateway and must be blocked"
)
}
}
Comment thread
amandagonsalves marked this conversation as resolved.

@Test
fun `BYPASS - 6to4 (2002__16) addresses embedding internal IPv4 are now blocked`() {
for (h in listOf("2002:a00:1::", "2002:7f00:1::")) {
assertTrue(
guardBlocks(h),
"$h reaches an internal IPv4 through a 6to4 relay and must be blocked"
)
}
}
Comment thread
amandagonsalves marked this conversation as resolved.

@Test
fun `additional - NAT64 local-use prefix (64_ff9b_1__48, RFC 8215) is blocked`() {
assertTrue(
guardBlocks("64:ff9b:1::a00:1"),
"64:ff9b:1::/48 is the RFC 8215 local-use NAT64 prefix and must be blocked outright"
)
}

@Test
fun `additional - 0_0_0_0_8 range is blocked`() {
assertTrue(guardBlocks("0.1.2.3"), "0.1.2.3 is in 0.0.0.0/8 and must be blocked")
}

@Test
fun `additional - ordinary IPv6 addresses outside __96 are not misclassified as IPv4-compatible`() {
assertFalse(
guardBlocks("::1:a00:1"),
"::1:a00:1 is an ordinary global IPv6 address, not an IPv4-compatible literal, even though" +
" its last 32 bits look like a private IPv4 address; it must not be blocked"
Comment thread
amandagonsalves marked this conversation as resolved.
)
}
}
Loading