update
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user