From 5a3a51033160e3539df661e7de3ee10b0d547c13 Mon Sep 17 00:00:00 2001 From: posimai Date: Thu, 9 Apr 2026 23:45:55 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20SSRF=20blocklist=20+=20=E3=83=AC?= =?UTF-8?q?=E3=82=B9=E3=83=9D=E3=83=B3=E3=82=B9=E3=82=B5=E3=82=A4=E3=82=BA?= =?UTF-8?q?=E5=88=B6=E9=99=90=20+=20DB=20pool=20max=2015=20+=20pool.on(err?= =?UTF-8?q?or)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index e0abcbff..52e10571 100644 --- a/server.js +++ b/server.js @@ -177,7 +177,13 @@ const pool = new Pool({ user: process.env.DB_USER || 'gitea', password: process.env.DB_PASSWORD || '', database: 'posimai_brain', - max: 5 + max: 15, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000, +}); +// プールレベルの接続エラーをキャッチ(未処理のままにしない) +pool.on('error', (err) => { + console.error('[DB] Unexpected pool error:', err.message); }); // ── Gemini ──────────────────────────────── @@ -282,15 +288,37 @@ function normalizeCharset(raw) { return 'utf-8'; } +// ── SSRF ガード(fetchMeta / fetchFullTextViaJina 共用)────────────── +// RFC 1918 プライベート帯域・ループバック・クラウドメタデータ IP をブロック +const SSRF_BLOCKED = /^(127\.|localhost$|::1$|0\.0\.0\.0$|169\.254\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|100\.100\.100\.100|metadata\.google\.internal)/i; +function isSsrfSafe(rawUrl) { + let parsed; + try { parsed = new URL(rawUrl); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + if (SSRF_BLOCKED.test(parsed.hostname)) return false; + return true; +} + // ── OGP フェッチ ─────────────────────────── +const FETCH_META_MAX_BYTES = 2 * 1024 * 1024; // 2 MB 上限 async function fetchMeta(url) { + if (!isSsrfSafe(url)) { + return { title: url.slice(0, 300), desc: '', ogImage: '', favicon: '' }; + } try { const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; PosimaiBot/1.0)' }, signal: AbortSignal.timeout(6000) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); - const buffer = await res.arrayBuffer(); + + // レスポンスサイズを 2MB に制限(OGP取得にそれ以上は不要) + const contentLength = parseInt(res.headers.get('content-length') || '0', 10); + if (contentLength > FETCH_META_MAX_BYTES) throw new Error('Response too large'); + const rawBuffer = await res.arrayBuffer(); + const buffer = rawBuffer.byteLength > FETCH_META_MAX_BYTES + ? rawBuffer.slice(0, FETCH_META_MAX_BYTES) + : rawBuffer; // 文字コード判定: 1) Content-Typeヘッダー優先 2) HTMLメタタグ確認 // iso-8859-1はバイト値0-255をロスレスでデコードするためcharset検出に最適 @@ -341,6 +369,7 @@ async function fetchMeta(url) { // ── Jina Reader API フェッチ(新規追加)─── async function fetchFullTextViaJina(url) { + if (!isSsrfSafe(url)) return null; try { console.log(`[Brain API] Fetching full text via Jina Reader for: ${url}`);