Skip to content

feat(scan): rate limit live scans per IP and per user - #135

Open
DevOlabode wants to merge 2 commits into
mainfrom
feat/scan-rate-limit
Open

DevOlabode wants to merge 2 commits into
mainfrom
feat/scan-rate-limit

Conversation

@DevOlabode

@DevOlabode DevOlabode commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Adds rate limiting to the live-scan routes, POST /api/scan and GET /api/scan-results. Every scan launches a headless Chromium, so without a limit, request volume was a cost and abuse risk.

  • Anonymous scans: limited per IP. 10 per 15 minutes. IPv6 addresses are grouped by subnet, so rotating addresses inside one allocation doesn't reset the count.
  • Signed-in scans: limited per user. 30 per 15 minutes. People sharing an office or campus IP each get their own budget, and signed-in traffic doesn't count against the IP's anonymous budget.
  • Both routes share one budget. GET /api/scan-results also runs a full scan. The results page uses it on refresh or a shared link, so left unlimited it would bypass the limit. A normal scan from the landing page counts once.
  • Blocked requests get 429 with code RATE_LIMITED and a Retry-After header, and Chromium never launches. The frontend already shows the server's error text.
  • Uses express-rate-limit, built per app in app.js (the composition root) and passed down to the scan router. dependencies.test.js now checks that it keeps a CommonJS entry, since Vercel can't require() ESM-only packages.

Known limit: counts live in memory, so each warm Vercel instance counts on its own and a cold start resets them. This is marked in the code with the upgrade path: a shared store via the library's store option.

The limits are constants at the top of backend/middleware/scanRateLimit.js, so they're easy to tune.

Closes #128

- Anonymous scans are limited per IP (IPv6 grouped by subnet); signed-in
  scans are limited per user so shared IPs don't share a budget
- Blocked requests get 429 RATE_LIMITED with Retry-After and never launch
  Chromium
- Guard that express-rate-limit keeps a CommonJS entry for Vercel
GET /api/scan-results also runs a live Chromium scan and is what the
results page uses on refresh or a shared link, so it bypassed the limit.
Both routes now share one limiter, so a scan costs one unit either way.
@DevOlabode DevOlabode changed the title feat(scan): rate limit POST /api/scan per IP and per user feat(scan): rate limit live scans per IP and per user Oct 19, 2026
@DevOlabode DevOlabode self-assigned this Oct 28, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add rate limiting on POST /api/scan

1 participant