'use strict'; // ============================================================================ // 요청 속도 제한 · 신뢰 프록시 기준 클라이언트 IP (v1.6.31, 요구 7·25) // // 1.5.x 까지는 X-Forwarded-For 첫 값을 그대로 믿어 공격자가 헤더를 위조하면 제한을 피할 수 있었다(검토 S12). // clientIp(req): 연결 상대(req.socket.remoteAddress)가 설정 service.trustedProxies(기본 127.0.0.1, ::1 — 같은 서버의 nginx)에 들어 있을 때만 // X-Forwarded-For 의 "신뢰 프록시 바로 앞" 값을 쓴다. 그 밖은 연결 상대 IP. // limited(key, max, windowMs): 슬라이딩 창. 메모리 보관(프로세스 재시작 때 비워짐 — 공격자가 재시작을 일으킬 수는 없다). // lockout(key, fails, windowMs): 로그인 실패 누적 잠금(계정 기준) — 비밀번호 대입 방지. // ============================================================================ const buckets = new Map(); // key → number[] (timestamps) let lastSweep = Date.now(); function sweep() { const now = Date.now(); if (now - lastSweep < 60000) return; lastSweep = now; for (const [k, arr] of buckets) { const keep = arr.filter((t) => now - t < 3600000); if (keep.length) buckets.set(k, keep); else buckets.delete(k); } } /** 창 안의 요청 수가 max 이상이면 true (그 요청은 거부). 거부된 요청도 횟수에 넣는다(연타 방지) */ function limited(key, max, windowMs) { sweep(); const now = Date.now(); const arr = (buckets.get(key) || []).filter((t) => now - t < windowMs); arr.push(now); buckets.set(key, arr); return arr.length > max; } function count(key, windowMs) { const now = Date.now(); return (buckets.get(key) || []).filter((t) => now - t < windowMs).length; } function reset(key) { buckets.delete(key); } /** 설정의 신뢰 프록시 목록 */ function trustedProxies() { try { const v = require('./config').get('service.trustedProxies'); if (v && String(v).trim()) return String(v).split(/[,\s]+/).filter(Boolean); } catch (_) {} return ['127.0.0.1', '::1', '::ffff:127.0.0.1']; } const norm = (ip) => String(ip || '').replace(/^::ffff:/, ''); function clientIp(req) { const peer = norm(req.socket && req.socket.remoteAddress); const trusted = trustedProxies().map(norm); if (!trusted.includes(peer)) return peer; const xff = String(req.headers['x-forwarded-for'] || '').split(',').map((s) => norm(s.trim())).filter(Boolean); // 오른쪽부터 신뢰 프록시를 벗겨 내고 처음 만나는 비신뢰 주소가 클라이언트 for (let i = xff.length - 1; i >= 0; i--) if (!trusted.includes(xff[i])) return xff[i]; return xff[0] || peer; } module.exports = { limited, count, reset, clientIp, trustedProxies };