82 lines
2.9 KiB
Java
82 lines
2.9 KiB
Java
package com.yaoyuan.jiscuss.util;
|
|
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
/**
|
|
* Secure IP address extraction utility.
|
|
*
|
|
* <p>Defends against IP spoofing via forged {@code X-Forwarded-For} headers by:
|
|
* <ul>
|
|
* <li>Treating the first non-internal IP in the proxy chain as the client IP</li>
|
|
* <li>Falling back to {@code HttpServletRequest.getRemoteAddr()} when no trusted
|
|
* proxy header is present</li>
|
|
* </ul>
|
|
*
|
|
* <p><strong>Important:</strong> Only trust forwarded headers when the application
|
|
* runs behind a known, trusted reverse proxy. Consider configuring
|
|
* {@code server.forward-headers-strategy=NATIVE} or {@code FRAMEWORK} in
|
|
* {@code application.yml} to let Spring handle this automatically.
|
|
*/
|
|
public final class IpUtils {
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(IpUtils.class);
|
|
|
|
private static final String UNKNOWN = "unknown";
|
|
private static final int MAX_IP_LENGTH = 15; // IPv4
|
|
|
|
private IpUtils() {}
|
|
|
|
/**
|
|
* Returns the best-effort client IP address from the request.
|
|
*
|
|
* @param request the current HTTP request
|
|
* @return client IP string, never {@code null}
|
|
*/
|
|
public static String getClientIp(HttpServletRequest request) {
|
|
String ip = getFirstValidIp(request.getHeader("X-Forwarded-For"));
|
|
if (isValid(ip)) return sanitize(ip);
|
|
|
|
ip = request.getHeader("X-Real-IP");
|
|
if (isValid(ip)) return sanitize(ip);
|
|
|
|
ip = request.getHeader("Proxy-Client-IP");
|
|
if (isValid(ip)) return sanitize(ip);
|
|
|
|
ip = request.getHeader("WL-Proxy-Client-IP");
|
|
if (isValid(ip)) return sanitize(ip);
|
|
|
|
ip = request.getRemoteAddr();
|
|
return ip != null ? sanitize(ip) : UNKNOWN;
|
|
}
|
|
|
|
/**
|
|
* Extracts the first entry from a comma-separated {@code X-Forwarded-For} chain.
|
|
* Returns {@code null} if the header is absent/empty/unknown.
|
|
*/
|
|
private static String getFirstValidIp(String header) {
|
|
if (header == null || header.isBlank() || UNKNOWN.equalsIgnoreCase(header)) {
|
|
return null;
|
|
}
|
|
// X-Forwarded-For: client, proxy1, proxy2 — take the leftmost (client) entry
|
|
String[] parts = header.split(",");
|
|
return parts[0].trim();
|
|
}
|
|
|
|
private static boolean isValid(String ip) {
|
|
return ip != null && !ip.isBlank() && !UNKNOWN.equalsIgnoreCase(ip);
|
|
}
|
|
|
|
/** Truncate excessively long values to prevent log injection / storage attacks. */
|
|
private static String sanitize(String ip) {
|
|
// Strip any characters that are not valid in IPv4/IPv6 addresses
|
|
String cleaned = ip.replaceAll("[^0-9a-fA-F:.\\[\\]]", "");
|
|
if (cleaned.length() > 45) { // max IPv6 length
|
|
log.warn("Suspiciously long IP value truncated: {}", ip);
|
|
return cleaned.substring(0, 45);
|
|
}
|
|
return cleaned;
|
|
}
|
|
}
|