mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 06:09:23 +00:00
feat(ssrf): enhance private IP detection and IPv6 handling (#4191)
# Description of Changes - Refactored `isPrivateAddress` to improve detection of private and local addresses for both IPv4 and IPv6. - Added explicit handling for: - IPv4-mapped IPv6 addresses - IPv6 link-local, site-local, and unique local (fc00::/7) addresses - Additional IPv4 private ranges such as link-local (169.254.0.0/16) - Introduced `normalizeIpv4MappedAddress` to standardize IP checks in cloud metadata detection. - Replaced `switch` statement with modern `switch` expression for cleaner control flow. These changes were made to strengthen SSRF protection by covering more address edge cases, especially in mixed IPv4/IPv6 environments. This also improves detection of cloud metadata endpoints when accessed via IPv4-mapped IPv6 addresses. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
f5f011f1e0
commit
40cf337b23
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.Inet6Address;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
@ -51,16 +53,12 @@ public class SsrfProtectionService {
|
|||||||
|
|
||||||
SsrfProtectionLevel level = parseProtectionLevel(config.getLevel());
|
SsrfProtectionLevel level = parseProtectionLevel(config.getLevel());
|
||||||
|
|
||||||
switch (level) {
|
return switch (level) {
|
||||||
case OFF:
|
case OFF -> true;
|
||||||
return true;
|
case MAX -> isMaxSecurityAllowed(trimmedUrl, config);
|
||||||
case MAX:
|
case MEDIUM -> isMediumSecurityAllowed(trimmedUrl, config);
|
||||||
return isMaxSecurityAllowed(trimmedUrl, config);
|
default -> false;
|
||||||
case MEDIUM:
|
};
|
||||||
return isMediumSecurityAllowed(trimmedUrl, config);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private SsrfProtectionLevel parseProtectionLevel(String level) {
|
private SsrfProtectionLevel parseProtectionLevel(String level) {
|
||||||
@ -172,15 +170,61 @@ public class SsrfProtectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPrivateAddress(InetAddress address) {
|
private boolean isPrivateAddress(InetAddress address) {
|
||||||
return address.isSiteLocalAddress()
|
if (address.isAnyLocalAddress() || address.isLoopbackAddress()) {
|
||||||
|| address.isAnyLocalAddress()
|
return true;
|
||||||
|| isPrivateIPv4Range(address.getHostAddress());
|
}
|
||||||
|
|
||||||
|
if (address instanceof Inet4Address) {
|
||||||
|
return isPrivateIPv4Range(address.getHostAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address instanceof Inet6Address addr6) {
|
||||||
|
if (addr6.isLinkLocalAddress() || addr6.isSiteLocalAddress()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = addr6.getAddress();
|
||||||
|
if (isIpv4MappedAddress(bytes)) {
|
||||||
|
String ipv4 =
|
||||||
|
(bytes[12] & 0xff)
|
||||||
|
+ "."
|
||||||
|
+ (bytes[13] & 0xff)
|
||||||
|
+ "."
|
||||||
|
+ (bytes[14] & 0xff)
|
||||||
|
+ "."
|
||||||
|
+ (bytes[15] & 0xff);
|
||||||
|
return isPrivateIPv4Range(ipv4);
|
||||||
|
}
|
||||||
|
|
||||||
|
int firstByte = bytes[0] & 0xff;
|
||||||
|
// Check for IPv6 unique local addresses (fc00::/7)
|
||||||
|
if ((firstByte & 0xfe) == 0xfc) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isIpv4MappedAddress(byte[] addr) {
|
||||||
|
if (addr.length != 16) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (addr[i] != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For IPv4-mapped IPv6 addresses, bytes 10 and 11 must be 0xff (i.e., address is ::ffff:w.x.y.z)
|
||||||
|
return addr[10] == (byte) 0xff && addr[11] == (byte) 0xff;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPrivateIPv4Range(String ip) {
|
private boolean isPrivateIPv4Range(String ip) {
|
||||||
|
// Includes RFC1918, loopback, link-local, and unspecified addresses
|
||||||
return ip.startsWith("10.")
|
return ip.startsWith("10.")
|
||||||
|| ip.startsWith("192.168.")
|
|| ip.startsWith("192.168.")
|
||||||
|| (ip.startsWith("172.") && isInRange172(ip))
|
|| (ip.startsWith("172.") && isInRange172(ip))
|
||||||
|
|| ip.startsWith("169.254.")
|
||||||
|| ip.startsWith("127.")
|
|| ip.startsWith("127.")
|
||||||
|| "0.0.0.0".equals(ip);
|
|| "0.0.0.0".equals(ip);
|
||||||
}
|
}
|
||||||
@ -192,17 +236,31 @@ public class SsrfProtectionService {
|
|||||||
int secondOctet = Integer.parseInt(parts[1]);
|
int secondOctet = Integer.parseInt(parts[1]);
|
||||||
return secondOctet >= 16 && secondOctet <= 31;
|
return secondOctet >= 16 && secondOctet <= 31;
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCloudMetadataAddress(String ip) {
|
private boolean isCloudMetadataAddress(String ip) {
|
||||||
|
String normalizedIp = normalizeIpv4MappedAddress(ip);
|
||||||
// Cloud metadata endpoints for AWS, GCP, Azure, Oracle Cloud, and IBM Cloud
|
// Cloud metadata endpoints for AWS, GCP, Azure, Oracle Cloud, and IBM Cloud
|
||||||
return ip.startsWith("169.254.169.254") // AWS/GCP/Azure
|
return normalizedIp.startsWith("169.254.169.254") // AWS/GCP/Azure
|
||||||
|| ip.startsWith("fd00:ec2::254") // AWS IPv6
|
|| normalizedIp.startsWith("fd00:ec2::254") // AWS IPv6
|
||||||
|| ip.startsWith("169.254.169.253") // Oracle Cloud
|
|| normalizedIp.startsWith("169.254.169.253") // Oracle Cloud
|
||||||
|| ip.startsWith("169.254.169.250"); // IBM Cloud
|
|| normalizedIp.startsWith("169.254.169.250"); // IBM Cloud
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeIpv4MappedAddress(String ip) {
|
||||||
|
if (ip == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (ip.startsWith("::ffff:")) {
|
||||||
|
return ip.substring(7);
|
||||||
|
}
|
||||||
|
int lastColon = ip.lastIndexOf(':');
|
||||||
|
if (lastColon >= 0 && ip.indexOf('.') > lastColon) {
|
||||||
|
return ip.substring(lastColon + 1);
|
||||||
|
}
|
||||||
|
return ip;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user