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 <noreply@anthropic.com>
This commit is contained in:
parent
426057960a
commit
7c6ecb77bc
|
|
@ -0,0 +1,3 @@
|
|||
.env
|
||||
.env.local
|
||||
node_modules/
|
||||
|
|
@ -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}`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue