package com.yaoyuan.jiscuss.util; import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Secure IP address extraction utility. * *

Defends against IP spoofing via forged {@code X-Forwarded-For} headers by: *

* *

Important: 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; } }