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}`);