Skip to content

[Security] Rate-limit bypass via spoofable X-Forwarded-For on lookup API #152

Description

@jefriidan15

Summary

I found a confirmed rate-limit bypass on the live lookup API of bug-bounties.as93.net. Read-only testing, no data modified.

Endpoint

https://bug-bounties.as93.net/api/lookup/website — same pattern applies to /api/lookup/{github|package|forge|app}.

Root cause

The in-memory rate limiter keys every request off the client-supplied X-Forwarded-For / X-Real-IP header, with no trusted-proxy validation.

web/src/lib/lookup/api-helpers.ts:

export function getClientIp(request: Request): string {
  return (
    request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
    request.headers.get("x-real-ip") ||
    "unknown"
  );
}
// enforceRateLimit(getClientIp(request)) → checkRateLimit(ip)  [Map keyed by ip]

An attacker who sets a fresh X-Forwarded-For value per request lands in a different IP bucket every time, so the configured caps (8/min, 100/hour, 300/day — rate-limit.ts) are never reached.

Live confirmation

  • Baseline (no spoof) — 9th request is blocked with 429:
    400 400 400 400 400 400 400 400 429 429 429 429
    
  • Spoofed X-Forwarded-For (unique value per request) — all 12 pass, 0 × 429:
    400 400 400 400 400 400 400 400 400 400 400 400
    

(The 400s are fabricated NXDOMAIN hosts — the limiter runs before domain resolution, so each still increments the counter.)

Impact

  • Unbounded scraping of the full program database.
  • Because every lookup fans out into several external fetches (homepage, DNS, security.txt, TLS, package registries), the endpoint also doubles as an open amplification/abuse vector against third-party looked-up targets.

Suggested fix

  • Use the real peer IP (Netlify exposes X-Nf-Client-Connection-IP; Cloudflare CF-Connecting-IP) instead of trusting the client header, and configure trusted-proxy so only the platform-appended forward header is honored.
  • Drop the client-controlled X-Real-IP fallback.
  • Note: the Map is single-instance and resets on redeploy — a shared store (e.g. Cloudflare KV) would also be more robust across edge instances.

Lower severity

web/src/pages/api/submit-anonymous.json.ts forwards with a hardcoded X-Gitgost-Key: gitgost-anonymous. I assume this is an intentional public anonymizer key; flagging in case you want the anonymous-submit path hardened.

Happy to provide a PoC script on request. Thanks for the clean codebase — the SSRF protections in resolve-domain.ts (DNS resolution + private-range blocking, including octal/hex/IPv4-mapped forms) are notably solid.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions