Skip to content
Open
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
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