All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Successful in 1m17s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 51s
Build & Deploy / 🔔 Notify (push) Successful in 2s
- Rate limit now uses the last X-Forwarded-For hop (appended by Traefik) instead of the client-controlled first hop, which bots rotated freely - Time-trap anchored to server time via HMAC-signed form token (FORM_TOKEN_SECRET) instead of client-supplied form_loaded_at - Fix RequestQuoteForm regression: FormData was built from scratch without honeypot/token hidden inputs and inputs lacked name attributes, so every legitimate quote request was silently blocked as spam
19 lines
630 B
TypeScript
19 lines
630 B
TypeScript
/**
|
|
* Extracts the client IP from the X-Forwarded-For header.
|
|
*
|
|
* Our edge proxy (Traefik) APPENDS the real peer IP to any client-supplied
|
|
* chain, so the LAST hop is the only trustworthy entry. The first hop is
|
|
* attacker-controlled: reading it lets bots rotate fake IPs per request and
|
|
* defeat IP-based rate limiting entirely.
|
|
*/
|
|
export function parseClientIp(xForwardedFor: string | null | undefined): string | null {
|
|
if (!xForwardedFor) {
|
|
return null;
|
|
}
|
|
const hops = xForwardedFor
|
|
.split(',')
|
|
.map((hop) => hop.trim())
|
|
.filter(Boolean);
|
|
return hops.length > 0 ? hops[hops.length - 1] : null;
|
|
}
|