fix(rate-limit): only trust X-Forwarded-For from known reverse proxies
Without this guard any client could send X-Forwarded-For: <spoofed-ip> and bypass per-IP rate limiting entirely. Also switches expireAfterWrite → expireAfterAccess so the 1-minute window starts at first request, not last, and fixes the .gitignore entry that accidentally merged **/test-results/ and .worktrees/ into one broken pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,10 +14,10 @@ public class RateLimitInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final int MAX_REQUESTS_PER_MINUTE = 10;
|
||||
|
||||
// Caffeine cache: per-IP counter that auto-expires after 1 minute of inactivity.
|
||||
// Caffeine cache: per-IP counter that expires 1 minute after first access.
|
||||
// Bounded to 10_000 entries to prevent OOM from IP exhaustion.
|
||||
private final Cache<String, AtomicInteger> requestCounts = Caffeine.newBuilder()
|
||||
.expireAfterWrite(1, TimeUnit.MINUTES)
|
||||
.expireAfterAccess(1, TimeUnit.MINUTES)
|
||||
.maximumSize(10_000)
|
||||
.build();
|
||||
|
||||
@@ -35,10 +35,23 @@ public class RateLimitInterceptor implements HandlerInterceptor {
|
||||
}
|
||||
|
||||
private String resolveClientIp(HttpServletRequest request) {
|
||||
String forwarded = request.getHeader("X-Forwarded-For");
|
||||
if (forwarded != null && !forwarded.isBlank()) {
|
||||
return forwarded.split(",")[0].trim();
|
||||
// Only trust X-Forwarded-For when the direct connection comes from a known
|
||||
// reverse proxy (loopback or Docker private network). Trusting it unconditionally
|
||||
// allows any client to spoof a different IP and bypass per-IP rate limiting.
|
||||
String remoteAddr = request.getRemoteAddr();
|
||||
if (isTrustedProxy(remoteAddr)) {
|
||||
String forwarded = request.getHeader("X-Forwarded-For");
|
||||
if (forwarded != null && !forwarded.isBlank()) {
|
||||
return forwarded.split(",")[0].trim();
|
||||
}
|
||||
}
|
||||
return request.getRemoteAddr();
|
||||
return remoteAddr;
|
||||
}
|
||||
|
||||
private boolean isTrustedProxy(String ip) {
|
||||
return ip.equals("127.0.0.1") || ip.equals("::1")
|
||||
|| ip.startsWith("10.")
|
||||
|| ip.startsWith("172.")
|
||||
|| ip.startsWith("192.168.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user