Adds two Workers rate-limit bindings keyed on client IP:
- RL_ENUM (30/min) on bare /{4hex} hits to throttle address scanning
- RL_UNLOCK (5/min per IP+address) on /__unlock to throttle password guessing
Asset subpaths are left unthrottled; over-limit returns 429 + Retry-After.- commit
- bb395f860b32fc4beaec085a9d933926ea9946e2
- parent
- d9e60d5b8e20dc2dca6381ea013c4358d0c02f3c
- author
- Dami <dami.etoile@gmail.com>
| M | worker/ | 4 | ++++ |
| M | worker/ | 21 | +++++++++++++++++++++ |
| M | worker/ | 4 | ++++ |
| M | worker/ | 4 | ++++ |
@@ -3,6 +3,10 @@ export interface Env {
33 DB: D1Database;44 NZIP_TOKEN: string;55 PUBLIC_BASE: string;6+ // Per-IP limiter for bare-address hits — throttles address enumeration.7+ RL_ENUM: RateLimit;8+ // Per-IP+address limiter for the unlock endpoint — throttles password guessing.9+ RL_UNLOCK: RateLimit;610 }711 812 export function json<T = unknown>(body: T, status = 200, headers: HeadersInit = {}): Response {@@ -5,6 +5,13 @@ import { err } from "./env.ts";
55 import { runGc } from "./gc.ts";66 import { serve } from "./serve.ts";77 8+function tooManyRequests(): Response {9+ return new Response("rate limited — slow down\n", {10+ status: 429,11+ headers: { "content-type": "text/plain; charset=utf-8", "retry-after": "60" },12+ });13+}14+815 export default {916 async fetch(req: Request, env: Env): Promise<Response> {1017 const url = new URL(req.url);@@ -18,6 +25,20 @@ export default {
1825 if (req.method !== "GET" && req.method !== "HEAD" && !isUnlock) {1926 return err("method not allowed", 405);2027 }28+29+ // Guessing defenses. The address space is only 16 bits, so throttle the two30+ // enumeration surfaces per client IP. Asset subpaths (/{addr}/...) are left31+ // alone — reaching them already requires knowing a live address.32+ const ip = req.headers.get("cf-connecting-ip") ?? "local";33+ if (isUnlock) {34+ const address = url.pathname.slice(1, 5);35+ const { success } = await env.RL_UNLOCK.limit({ key: `${ip}:${address}` });36+ if (!success) return tooManyRequests();37+ } else if (/^\/[0-9a-f]{4}\/?$/.test(url.pathname)) {38+ const { success } = await env.RL_ENUM.limit({ key: ip });39+ if (!success) return tooManyRequests();40+ }41+2142 return await serve(req, env, url);2243 },2344 @@ -16,6 +16,10 @@
1616 "database_id": "00000000-0000-0000-0000-000000000000"1717 }1818 ],19+ "ratelimits": [20+ { "name": "RL_ENUM", "namespace_id": "1001", "simple": { "limit": 30, "period": 60 } },21+ { "name": "RL_UNLOCK", "namespace_id": "1002", "simple": { "limit": 5, "period": 60 } }22+ ],1923 "triggers": {2024 "crons": [2125 "0 4 * * *"@@ -17,6 +17,10 @@
1717 "database_id": "00000000-0000-0000-0000-000000000000"1818 }1919 ],20+ "ratelimits": [21+ { "name": "RL_ENUM", "namespace_id": "1001", "simple": { "limit": 30, "period": 60 } },22+ { "name": "RL_UNLOCK", "namespace_id": "1002", "simple": { "limit": 5, "period": 60 } }23+ ],2024 "triggers": {2125 "crons": [2226 "0 4 * * *"