From 7c6ecb77bc08c0106fe6a1d96dd9bbba5c572c88 Mon Sep 17 00:00:00 2001 From: posimai Date: Fri, 24 Apr 2026 16:44:01 +0900 Subject: [PATCH] security(dev): SSRF fix, WS limit, log rotation, BIND_HOST, sw.js API cache skip, .gitignore - /api/check: add requireLocal + block 100.x/IPv6 in SSRF filter - WebSocket: limit concurrent sessions to 3 - Session logs: auto-prune after 30 days - server.listen: respect BIND_HOST env var - sw.js: exclude /api/* from cache - .gitignore: protect .env and node_modules Co-Authored-By: Claude Sonnet 4.6 --- posimai-dev/.gitignore | 3 +++ posimai-dev/server.js | 30 ++++++++++++++++++++++++++---- posimai-dev/sw.js | 3 ++- 3 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 posimai-dev/.gitignore diff --git a/posimai-dev/.gitignore b/posimai-dev/.gitignore new file mode 100644 index 00000000..487eca42 --- /dev/null +++ b/posimai-dev/.gitignore @@ -0,0 +1,3 @@ +.env +.env.local +node_modules/ diff --git a/posimai-dev/server.js b/posimai-dev/server.js index f0f7dc3f..84106243 100644 --- a/posimai-dev/server.js +++ b/posimai-dev/server.js @@ -25,6 +25,21 @@ const PORT = process.env.PORT || 3333; const SESSIONS_DIR = path.join(os.homedir(), 'posimai-dev-sessions'); if (!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true }); +// 30日超のセッションログを削除 +function pruneSessionLogs() { + const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; + try { + fs.readdirSync(SESSIONS_DIR) + .filter(f => f.endsWith('.log')) + .forEach(f => { + const full = path.join(SESSIONS_DIR, f); + if (fs.statSync(full).mtimeMs < cutoff) fs.unlinkSync(full); + }); + } catch (_) {} +} +pruneSessionLogs(); +setInterval(pruneSessionLogs, 24 * 60 * 60 * 1000); + app.use(express.json()); app.use(express.static(path.join(__dirname))); @@ -235,7 +250,7 @@ app.get('/api/vps-health', async (req, res) => { // ── サービス死活チェックプロキシ (/api/check?url=...) ────────── // ブラウザの mixed-content 制限を回避するためサーバー側から HTTP チェック // SSRF 対策: http/https のみ許可、クラウドメタデータ IP をブロック -const BLOCKED_HOSTS = /^(127\.|localhost$|::1$|169\.254\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|100\.100\.100\.100|metadata\.google\.internal)/i; +const BLOCKED_HOSTS = /^(127\.|localhost$|::1$|\[::1\]|0\.0\.0\.0|169\.254\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|100\.|metadata\.google\.internal)/i; function isCheckUrlAllowed(raw) { let parsed; try { parsed = new URL(raw); } catch { return false; } @@ -244,7 +259,7 @@ function isCheckUrlAllowed(raw) { return true; } -app.get('/api/check', async (req, res) => { +app.get('/api/check', requireLocal, async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); const { url } = req.query; if (!url) return res.status(400).json({ ok: false, error: 'url required' }); @@ -286,6 +301,12 @@ wss.on('connection', (ws, req) => { return; } + // 同時セッション数を制限(自分自身を含むので size > 3 で実質3セッション上限) + if (wss.clients.size > 3) { + ws.close(1013, 'Too many sessions'); + return; + } + // セッションID・ログファイル作成 const sessionId = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const logPath = path.join(SESSIONS_DIR, `${sessionId}.log`); @@ -330,6 +351,7 @@ wss.on('connection', (ws, req) => { ptyProc.onExit(() => { try { ws.close(); } catch {} }); }); -server.listen(PORT, '0.0.0.0', () => { - console.log(`posimai-dev running on ${proto}://0.0.0.0:${PORT}`); +const BIND_HOST = process.env.BIND_HOST || '0.0.0.0'; +server.listen(PORT, BIND_HOST, () => { + console.log(`posimai-dev running on ${proto}://${BIND_HOST}:${PORT}`); }); diff --git a/posimai-dev/sw.js b/posimai-dev/sw.js index af5ddd10..3e2c5172 100644 --- a/posimai-dev/sw.js +++ b/posimai-dev/sw.js @@ -15,9 +15,10 @@ self.addEventListener('activate', (e) => { self.clients.claim(); }); -// ターミナルのWebSocket通信はキャッシュしない +// WebSocket・API レスポンスはキャッシュしない self.addEventListener('fetch', (e) => { if (e.request.url.includes('/terminal')) return; + if (new URL(e.request.url).pathname.startsWith('/api/')) return; e.respondWith( caches.match(e.request).then((r) => r || fetch(e.request)) );