aboutsummaryrefslogtreecommitdiff
path: root/docs/vercel-firewall.md
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-06-02 01:56:02 +0000
committerFuwn <[email protected]>2026-06-02 01:56:02 +0000
commit6dc809d833212a1b5ff75786bfacfb4581dac1a2 (patch)
tree598fd32eef9eba6047ee4973912666aac77ff729 /docs/vercel-firewall.md
parentfix(security): sanitize badge_wall_css server-side, render via textContent (diff)
downloaddue.moe-6dc809d833212a1b5ff75786bfacfb4581dac1a2.tar.xz
due.moe-6dc809d833212a1b5ff75786bfacfb4581dac1a2.zip
fix(security): make rate limiting real; limit the click counter (L8/L10)
rateLimit.ts built a new in-memory RateLimiter inside the function on every call, so its store was always empty and it never limited anything. Rewrite as module-level singletons: auth/mutation/read IP classes returning 429, plus a click-counter limiter keyed on (IP, badge) so a viewer browsing many different badges isn't throttled while hammering one badge is capped. Wire the counter into PUT /api/badges?incrementClickCount (L10). Add a pluggable RateLimiterStore seam (in-memory default, Upstash/Vercel KV ready) and document the serverless per-region caveat. Add docs/vercel-firewall.md with the dashboard WAF rate-limit rule spec (Hobby 1-rule vs Pro) for coarse per-IP edge protection — keys the app limiters can't express at the edge. Verified locally: same-badge hits 200,200,429,429…; a different badge stays 200; a no-origin hit is rate-checked then 401.
Diffstat (limited to 'docs/vercel-firewall.md')
-rw-r--r--docs/vercel-firewall.md43
1 files changed, 43 insertions, 0 deletions
diff --git a/docs/vercel-firewall.md b/docs/vercel-firewall.md
new file mode 100644
index 00000000..0d3fdbff
--- /dev/null
+++ b/docs/vercel-firewall.md
@@ -0,0 +1,43 @@
+# Vercel WAF — rate-limiting rules
+
+Coarse, per-IP abuse protection that runs at the edge, complementing the
+app-level limiters in `src/lib/Error/rateLimit.ts` (which handle what the WAF
+cannot — per-user windows and the per-(IP, badge) click counter).
+
+These rules are **not committable config** — Vercel WAF rules are managed in the
+dashboard (or via Terraform / the REST API), not `vercel.json`. This file is the
+spec to enter under **Project → Firewall → Configure → + New Rule**.
+
+Caveats from the platform:
+
+- Counters are **per region** — traffic spread across regions can exceed a
+ single-region limit.
+- Counting keys on Hobby/Pro are **IP / JA4 only** (no app data like a badge id;
+ that is why the click counter is limited app-side instead).
+- Window: min **10s**, max **10min** (Hobby/Pro).
+- **Hobby allows only 1 rule per project**; Pro allows 40.
+- Start each rule's action as **Log** to observe real traffic, then switch to
+ **Deny (429)** once the thresholds look right.
+
+## Hobby (single rule)
+
+| Field | Value |
+| --- | --- |
+| Name | `api-mutations-ratelimit` |
+| If | `Request Path` starts with `/api/` **AND** `Request Method` is one of `POST`, `PUT`, `DELETE` |
+| Then | Rate Limit — Fixed Window |
+| Window / Limit | `60s` / `100` requests |
+| Key | `IP` |
+| Action | `Deny` (429) |
+
+## Pro (targeted rules)
+
+| Name | If | Window / Limit | Key | Action |
+| --- | --- | --- | --- | --- |
+| `auth-ratelimit` | Path starts with `/api/oauth/` | `60s` / `20` | IP | Deny |
+| `mutations-ratelimit` | Path is one of `/api/badges`, `/api/preferences`, `/api/configuration` **or** starts with `/api/notifications/`; Method `POST`/`PUT`/`DELETE` | `60s` / `60` | IP | Deny |
+| `graphql-ratelimit` | Path is `/graphql`; Method `POST` | `60s` / `60` | IP | Deny |
+
+Public GET reads and the click counter are left to the CDN cache and the
+app-level limiters respectively; add a coarse `/api/*` GET rule only if you see
+read abuse.