mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 13:20:08 +03:00
Compare commits
1
Commits
format_java
...
urlFixes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a0d31bd0c |
+93
-12
@@ -155,8 +155,7 @@ public class SsrfProtectionService {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.isBlockCloudMetadata()
|
||||
&& isCloudMetadataAddress(address.getHostAddress())) {
|
||||
if (config.isBlockCloudMetadata() && isCloudMetadataAddress(address)) {
|
||||
log.debug("URL blocked - cloud metadata endpoint: {}", url);
|
||||
return false;
|
||||
}
|
||||
@@ -189,16 +188,9 @@ public class SsrfProtectionService {
|
||||
}
|
||||
|
||||
byte[] bytes = addr6.getAddress();
|
||||
if (isIpv4MappedAddress(bytes)) {
|
||||
String ipv4 =
|
||||
(bytes[12] & 0xff)
|
||||
+ "."
|
||||
+ (bytes[13] & 0xff)
|
||||
+ "."
|
||||
+ (bytes[14] & 0xff)
|
||||
+ "."
|
||||
+ (bytes[15] & 0xff);
|
||||
return isPrivateIPv4Range(ipv4);
|
||||
String embeddedIpv4 = extractEmbeddedIpv4(bytes);
|
||||
if (embeddedIpv4 != null) {
|
||||
return isPrivateIPv4Range(embeddedIpv4);
|
||||
}
|
||||
|
||||
int firstByte = bytes[0] & 0xff;
|
||||
@@ -211,6 +203,30 @@ public class SsrfProtectionService {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dotted-quad IPv4 embedded in an IPv6 address that wraps an IPv4 destination, or
|
||||
* null if the address is not an embedded-IPv4 form. Covers IPv4-mapped (::ffff:0:0/96),
|
||||
* IPv4-compatible (::/96, deprecated), NAT64 well-known prefix (64:ff9b::/96, RFC 6052), and
|
||||
* 6to4 (2002::/16, RFC 3056). NAT64 and 6to4 are global-unicast prefixes that no JDK classifier
|
||||
* flags as private, so the embedded IPv4 must be re-checked against the private/reserved IPv4
|
||||
* ranges to keep the SSRF guard sound.
|
||||
*/
|
||||
private String extractEmbeddedIpv4(byte[] bytes) {
|
||||
if (bytes == null || bytes.length != 16) {
|
||||
return null;
|
||||
}
|
||||
if (isIpv4MappedAddress(bytes) || isIpv4CompatibleAddress(bytes)) {
|
||||
return formatIpv4(bytes, 12);
|
||||
}
|
||||
if (isNat64Address(bytes)) {
|
||||
return formatIpv4(bytes, 12);
|
||||
}
|
||||
if (isSixToFourAddress(bytes)) {
|
||||
return formatIpv4(bytes, 2);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isIpv4MappedAddress(byte[] addr) {
|
||||
if (addr.length != 16) {
|
||||
return false;
|
||||
@@ -225,6 +241,56 @@ public class SsrfProtectionService {
|
||||
return addr[10] == (byte) 0xff && addr[11] == (byte) 0xff;
|
||||
}
|
||||
|
||||
private boolean isIpv4CompatibleAddress(byte[] addr) {
|
||||
// ::/96 deprecated IPv4-compatible IPv6 (e.g., ::169.254.169.254). All-zero first 12 bytes
|
||||
// and a non-zero embedded IPv4 (an all-zero address would be the unspecified address, not
|
||||
// an embedded IPv4 and already caught by isAnyLocalAddress).
|
||||
if (addr.length != 16) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < 12; i++) {
|
||||
if (addr[i] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return addr[12] != 0 || addr[13] != 0 || addr[14] != 0 || addr[15] != 0;
|
||||
}
|
||||
|
||||
private boolean isNat64Address(byte[] addr) {
|
||||
// NAT64 well-known prefix 64:ff9b::/96 (RFC 6052) - first 12 bytes are 00 64 ff 9b 00 00
|
||||
// ...
|
||||
if (addr.length != 16) {
|
||||
return false;
|
||||
}
|
||||
if (addr[0] != 0x00
|
||||
|| addr[1] != 0x64
|
||||
|| addr[2] != (byte) 0xff
|
||||
|| addr[3] != (byte) 0x9b) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 4; i < 12; i++) {
|
||||
if (addr[i] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isSixToFourAddress(byte[] addr) {
|
||||
// 6to4 prefix 2002::/16 (RFC 3056) - embedded IPv4 is in bytes[2..5]
|
||||
return addr.length == 16 && addr[0] == 0x20 && addr[1] == 0x02;
|
||||
}
|
||||
|
||||
private String formatIpv4(byte[] addr, int offset) {
|
||||
return (addr[offset] & 0xff)
|
||||
+ "."
|
||||
+ (addr[offset + 1] & 0xff)
|
||||
+ "."
|
||||
+ (addr[offset + 2] & 0xff)
|
||||
+ "."
|
||||
+ (addr[offset + 3] & 0xff);
|
||||
}
|
||||
|
||||
private boolean isPrivateIPv4Range(String ip) {
|
||||
// Includes RFC1918, RFC6598, loopback, link-local, and unspecified addresses
|
||||
return ip.startsWith("10.")
|
||||
@@ -260,6 +326,21 @@ public class SsrfProtectionService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isCloudMetadataAddress(InetAddress address) {
|
||||
if (isCloudMetadataAddress(address.getHostAddress())) {
|
||||
return true;
|
||||
}
|
||||
// Also unwrap NAT64/6to4/IPv4-compat embedded IPv4 so cloud metadata IPs reached via an
|
||||
// IPv6 prefix are matched even when blockPrivateNetworks is disabled.
|
||||
if (address instanceof Inet6Address) {
|
||||
String embedded = extractEmbeddedIpv4(address.getAddress());
|
||||
if (embedded != null && isCloudMetadataAddress(embedded)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isCloudMetadataAddress(String ip) {
|
||||
String normalizedIp = normalizeIpv4MappedAddress(ip);
|
||||
// Cloud metadata endpoints for AWS, GCP, Azure, Oracle Cloud, and IBM Cloud
|
||||
|
||||
@@ -402,6 +402,19 @@ public class GeneralUtils {
|
||||
Arrays.copyOfRange(rawAddress, rawAddress.length - 4, rawAddress.length);
|
||||
return isPrivateOrReservedIPv4(ipv4);
|
||||
}
|
||||
// NAT64 well-known prefix 64:ff9b::/96 (RFC 6052) - global-unicast IPv6 that wraps an
|
||||
// IPv4 destination; re-check the embedded IPv4 against the private/reserved table so
|
||||
// 64:ff9b::a9fe:a9fe (169.254.169.254) and friends are blocked.
|
||||
if (isNat64Address(rawAddress)) {
|
||||
byte[] ipv4 =
|
||||
Arrays.copyOfRange(rawAddress, rawAddress.length - 4, rawAddress.length);
|
||||
return isPrivateOrReservedIPv4(ipv4);
|
||||
}
|
||||
// 6to4 prefix 2002::/16 (RFC 3056) - the next 32 bits embed the IPv4.
|
||||
if (isSixToFourAddress(rawAddress)) {
|
||||
byte[] ipv4 = Arrays.copyOfRange(rawAddress, 2, 6);
|
||||
return isPrivateOrReservedIPv4(ipv4);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -497,6 +510,43 @@ public class GeneralUtils {
|
||||
return address[10] == (byte) 0xFF && address[11] == (byte) 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an IPv6 address sits in the NAT64 well-known prefix 64:ff9b::/96 (RFC 6052).
|
||||
* No JDK classifier flags this prefix as private, so the embedded IPv4 in bytes[12..15] must be
|
||||
* re-checked against the private/reserved IPv4 table.
|
||||
*
|
||||
* @param address 16-byte IPv6 address
|
||||
* @return {@code true} if the address is in 64:ff9b::/96
|
||||
*/
|
||||
private boolean isNat64Address(byte[] address) {
|
||||
if (address == null || address.length != 16) {
|
||||
return false;
|
||||
}
|
||||
if (address[0] != 0x00
|
||||
|| address[1] != 0x64
|
||||
|| address[2] != (byte) 0xFF
|
||||
|| address[3] != (byte) 0x9B) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 4; i < 12; i++) {
|
||||
if (address[i] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an IPv6 address sits in the 6to4 prefix 2002::/16 (RFC 3056). The embedded
|
||||
* IPv4 lives in bytes[2..5] and must be re-checked against the private/reserved IPv4 table.
|
||||
*
|
||||
* @param address 16-byte IPv6 address
|
||||
* @return {@code true} if the address is in 2002::/16
|
||||
*/
|
||||
private boolean isSixToFourAddress(byte[] address) {
|
||||
return address != null && address.length == 16 && address[0] == 0x20 && address[1] == 0x02;
|
||||
}
|
||||
|
||||
/*
|
||||
* Improved multipart file conversion using the shared helper method.
|
||||
*
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
class SsrfProtectionServiceTest {
|
||||
|
||||
private SsrfProtectionService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new SsrfProtectionService(new ApplicationProperties());
|
||||
}
|
||||
|
||||
// Regression for GHSA-3x2q-gcww-hpj9: NAT64 (64:ff9b::/96, RFC 6052), 6to4 (2002::/16,
|
||||
// RFC 3056), and IPv4-compatible (::/96, deprecated) addresses embed an IPv4 inside a
|
||||
// global-unicast IPv6 prefix. No JDK classifier flags them, so the guard must unwrap the
|
||||
// embedded IPv4 and re-check it against the private/reserved table. The advisory verified that
|
||||
// 64:ff9b::a9fe:a9fe (= 169.254.169.254) survived the guard at v2.11.0.
|
||||
@ParameterizedTest
|
||||
@ValueSource(
|
||||
strings = {
|
||||
"http://169.254.169.254/latest/meta-data/",
|
||||
"http://127.0.0.1/internal",
|
||||
"http://[::ffff:169.254.169.254]/x",
|
||||
"http://[::ffff:127.0.0.1]/x",
|
||||
"http://[::169.254.169.254]/x",
|
||||
"http://[64:ff9b::a9fe:a9fe]/latest/meta-data/",
|
||||
"http://[64:ff9b::7f00:1]/internal",
|
||||
"http://[64:ff9b::a9fe:a9fd]/",
|
||||
"http://[64:ff9b::a9fe:a9fa]/",
|
||||
"http://[2002:a9fe:a9fe::]/latest/meta-data/",
|
||||
"http://[2002:7f00:1::]/internal",
|
||||
"http://[fd00::1]/",
|
||||
"http://[fe80::1]/",
|
||||
})
|
||||
void blocksInternalAndEmbeddedIpv4Forms(String url) {
|
||||
assertFalse(service.isUrlAllowed(url), () -> "Expected guard to block " + url);
|
||||
}
|
||||
|
||||
// NAT64/6to4 wrappers around PUBLIC IPv4s must remain allowed - the fix must not over-block.
|
||||
// 64:ff9b::808:808 and 2002:808:808:: both wrap 8.8.8.8 (Google DNS, public).
|
||||
@ParameterizedTest
|
||||
@ValueSource(
|
||||
strings = {
|
||||
"http://[64:ff9b::808:808]/",
|
||||
"http://[2002:808:808::]/",
|
||||
})
|
||||
void allowsNat64AndSixToFourOfPublicIpv4(String url) {
|
||||
assertTrue(service.isUrlAllowed(url), () -> "Expected guard to allow " + url);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user