Detail Bug Report
https://app.detail.dev/org_befd6425-a158-4e24-9d4d-1e5c08769515/bugs/bug_d9dbf8cc-ab48-4e5d-92e0-40f382ee49d6
Introduced in #161 by @WilliamAGH on Aug 1, 2026
Summary
- Context: The contact form rate limiting uses the client IP address (obtained via
HttpServletRequest.getRemoteAddr()) as the key for a Caffeine cache that limits submissions to 3 per hour per IP.
- Bug: An attacker can bypass the rate limit by sending arbitrary
X-Forwarded-For headers, making each request appear to come from a different IP address.
- Actual vs. expected: An attacker can send unlimited contact form submissions, when only 3 per hour per real IP should be allowed.
- Impact: The rate limiting mechanism is completely ineffective, allowing unlimited spam/abuse of the contact form.
Code with Bug
// ContactSubmissionUseCase.java - rate limiting uses remoteAddress as cache key
AtomicInteger acceptedSubmissionCount =
acceptedSubmissionsPerIp.get(contactSubmission.remoteAddress(), remoteAddress -> new AtomicInteger()); // <-- BUG 🔴 key is spoofable if remoteAddress comes from forwarded header
int reservedSubmissionSlot = acceptedSubmissionCount.incrementAndGet();
if (reservedSubmissionSlot > MAX_ACCEPTED_SUBMISSIONS_PER_IP) {
acceptedSubmissionCount.decrementAndGet();
throw new ContactRateLimitExceededException();
}
# application.properties - enables forwarded header processing without trusted proxy configuration
server.forward-headers-strategy=framework # <-- BUG 🔴 trusts client-supplied X-Forwarded-For unless proxies are restricted
// ContactController.java - extracts IP directly from servlet request without validation
servletRequest.getRemoteAddr(), // <-- BUG 🔴 value can be influenced by X-Forwarded-For when ForwardedHeaderFilter is enabled
Explanation
Spring’s ForwardedHeaderFilter is enabled via server.forward-headers-strategy=framework, which causes HttpServletRequest.getRemoteAddr() to be derived from X-Forwarded-For. Because there is no trusted-proxy restriction (e.g., server.tomcat.internal-proxies), any direct client can supply arbitrary X-Forwarded-For values.
Since the rate limiter uses the request “remote address” as its cache key, an attacker can rotate spoofed IPs per request and never hit the per-IP limit.
Evidence: an integration test (ContactControllerIpSpoofingTest) loads the full app context (including forwarded header processing), fetches a real CSRF token from /api/security/csrf, then submits 10 contact requests each with a different X-Forwarded-For; all 10 are accepted and 10 emails are sent.
Exploit Scenario
- Attacker requests
GET /api/security/csrf to obtain a CSRF token cookie.
- Attacker waits 3+ seconds to satisfy the render-time trap.
- Attacker sends
POST /api/contact with valid CSRF headers/cookie and a spoofed X-Forwarded-For (e.g., 192.0.2.1).
- Attacker repeats the POST, changing
X-Forwarded-For each time (e.g., 192.0.2.2, 192.0.2.3, ...).
- Each request is treated as a new IP, allowing unlimited submissions.
Recommended Fix
Restrict forwarded-header trust to known proxy IP ranges (or disable forwarded header processing if no reverse proxy is used). Example:
server.forward-headers-strategy=framework
server.tomcat.internal-proxies=10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|192\\.168\\.\\d{1,3}\\.\\d{1,3}|172\\.(1[6-9]|2[0-9]|3[0-1])\\.\\d{1,3}\\.\\d{1,3}
History
This bug was introduced in commit e34b631. The commit added IP-based rate limiting for the contact form using getRemoteAddr() without configuring trusted proxies, so the existing server.forward-headers-strategy=framework setting (added in 25bd9a4 to handle reverse proxy deployments) blindly trusts all X-Forwarded-For headers, allowing spoofing.
Detail Bug Report
https://app.detail.dev/org_befd6425-a158-4e24-9d4d-1e5c08769515/bugs/bug_d9dbf8cc-ab48-4e5d-92e0-40f382ee49d6
Introduced in #161 by @WilliamAGH on Aug 1, 2026
Summary
HttpServletRequest.getRemoteAddr()) as the key for a Caffeine cache that limits submissions to 3 per hour per IP.X-Forwarded-Forheaders, making each request appear to come from a different IP address.Code with Bug
Explanation
Spring’s
ForwardedHeaderFilteris enabled viaserver.forward-headers-strategy=framework, which causesHttpServletRequest.getRemoteAddr()to be derived fromX-Forwarded-For. Because there is no trusted-proxy restriction (e.g.,server.tomcat.internal-proxies), any direct client can supply arbitraryX-Forwarded-Forvalues.Since the rate limiter uses the request “remote address” as its cache key, an attacker can rotate spoofed IPs per request and never hit the per-IP limit.
Evidence: an integration test (
ContactControllerIpSpoofingTest) loads the full app context (including forwarded header processing), fetches a real CSRF token from/api/security/csrf, then submits 10 contact requests each with a differentX-Forwarded-For; all 10 are accepted and 10 emails are sent.Exploit Scenario
GET /api/security/csrfto obtain a CSRF token cookie.POST /api/contactwith valid CSRF headers/cookie and a spoofedX-Forwarded-For(e.g.,192.0.2.1).X-Forwarded-Foreach time (e.g.,192.0.2.2,192.0.2.3, ...).Recommended Fix
Restrict forwarded-header trust to known proxy IP ranges (or disable forwarded header processing if no reverse proxy is used). Example:
History
This bug was introduced in commit e34b631. The commit added IP-based rate limiting for the contact form using
getRemoteAddr()without configuring trusted proxies, so the existingserver.forward-headers-strategy=frameworksetting (added in 25bd9a4 to handle reverse proxy deployments) blindly trusts allX-Forwarded-Forheaders, allowing spoofing.