Changeset 75ab393 in Klonkt
- Timestamp:
- 06/25/2026 08:45:02 PM (3 months ago)
- Branches:
- main
- Children:
- a9900ec
- Parents:
- 49edc72
- Location:
- src
- Files:
-
- 2 edited
-
middleware/rate-limit.js (modified) (2 diffs)
-
routes/activitypub.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/middleware/rate-limit.js
r49edc72 r75ab393 21 21 function clientKey(req) { 22 22 let ip = req.ip || req.socket?.remoteAddress || ''; 23 // Strip a trailing IPv4 port (1.2.3.4:11046 -> 1.2.3.4) 23 24 if (/^\d{1,3}(\.\d{1,3}){3}:\d+$/.test(ip)) ip = ip.split(':')[0]; 25 // IPv6-mapped IPv4 (::ffff:1.2.3.4) -> the plain IPv4 26 const mapped = ip.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i); 27 if (mapped) return mapped[1]; 28 // Real IPv6: key on the /64 network prefix, not the full address. A single 29 // user/allocation is usually a whole /64, so this stops an attacker from 30 // getting a fresh budget by rotating addresses within their own range. 31 if (ip.includes(':')) { 32 const left = ip.includes('::') ? ip.split('::')[0] : ip; 33 const groups = left.split(':').filter(Boolean); 34 while (groups.length < 4) groups.push('0'); 35 return groups.slice(0, 4).join(':') + '::/64'; 36 } 24 37 return ip || 'unknown'; 25 38 } … … 70 83 handler: blockedHandler('pages/auth-register', 'on-special', 'Too many signup attempts.'), 71 84 }); 85 86 // ─── Fediverse (/ap/*) ──────────────────────────────────────────── 87 // These endpoints are hit by REMOTE SERVERS, not browsers, so the default 88 // plain-text 429 is the right response (no HTML page). Deliberately generous: 89 // legitimate federation from one instance never comes close, but a flood from 90 // a single IP is capped. Per-IP via the same /64-aware clientKey. 91 92 // Baseline read cap across all /ap/* (actor, outbox, notes, webfinger, …). 93 // 5 req/sec per IP — far above any real Mastodon polling. 94 export const apReadLimiter = rateLimit({ 95 windowMs: 60 * 1000, 96 max: 300, 97 standardHeaders: true, 98 legacyHeaders: false, 99 keyGenerator: clientKey, 100 validate: { ip: false }, 101 }); 102 103 // Inbox POSTs each trigger an outbound actor fetch (signature verify) → cap the 104 // amplification/queue-inflation a single source can drive. 120/min/IP is still 105 // generous for a small site's inbound federation; bump if a busy instance trips it. 106 export const apInboxLimiter = rateLimit({ 107 windowMs: 60 * 1000, 108 max: 120, 109 standardHeaders: true, 110 legacyHeaders: false, 111 keyGenerator: clientKey, 112 validate: { ip: false }, 113 }); -
src/routes/activitypub.js
r49edc72 r75ab393 16 16 import db from '../config/database.js'; 17 17 import AP from '../services/ActivityPubService.js'; 18 import { apReadLimiter, apInboxLimiter } from '../middleware/rate-limit.js'; 18 19 19 20 const router = express.Router(); 21 // Generous per-IP baseline over all /ap/* (reads). The inbox POST gets an 22 // additional, tighter cap inline (it triggers outbound fetches). 23 router.use(apReadLimiter); 20 24 let _ver = '1.0.0'; 21 25 try { _ver = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url))).version || _ver; } catch { /* keep default */ } … … 160 164 verify: (req, _res, buf) => { req.rawBody = buf; }, // raw body for digest verification 161 165 }); 162 router.post(['/ap/users/:slug/inbox', '/ap/inbox'], ap Json, async (req, res) => {166 router.post(['/ap/users/:slug/inbox', '/ap/inbox'], apInboxLimiter, apJson, async (req, res) => { 163 167 try { return res.status(await AP.handleInbox(req, req.params.slug || null) || 202).end(); } 164 168 catch (e) { console.warn('[AP inbox] error:', e.message); return res.status(202).end(); }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)