From 9e6a2987ed7a1d1b0f64d7855c6ef7ba4a3020fc Mon Sep 17 00:00:00 2001 From: posimai Date: Fri, 3 Apr 2026 08:15:45 +0900 Subject: [PATCH] fix: security hardening - XSS, SSRF, proxy auth, Syncthing config - server.js: add escapeHtml() and apply to meta.title / error messages (XSS) - server.js: add startup error log when JWT_SECRET uses insecure default - posimai-dev/server.js: add URL validation to /api/check to block SSRF (blocks cloud metadata IPs, non-http/https protocols) - ponshu_room_lite/tools/proxy/server.js: remove auth bypass when PROXY_AUTH_TOKEN is unset; server now exits on startup if token missing - .gitignore: add *.sync-conflict-* to prevent Syncthing conflict files - .stignore: create Syncthing ignore file to exclude .git, node_modules, .env from sync (fixes root cause of .git directory sync-conflict files) Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 3 +++ .stignore | 20 ++++++++++++++++++++ posimai-dev/server.js | 11 +++++++++++ server.js | 17 +++++++++++++++-- 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 .stignore diff --git a/.gitignore b/.gitignore index 4d65e5c7..6398ca90 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ .vercel +# Syncthing コンフリクトファイル +*.sync-conflict-* + # 一時ファイル・バックアップ(ルート) _tmp_* *.backup* diff --git a/.stignore b/.stignore new file mode 100644 index 00000000..9a963697 --- /dev/null +++ b/.stignore @@ -0,0 +1,20 @@ +// Syncthing ignore file for posimai-project +// IMPORTANT: .git を同期すると git インデックスが破損するため必須 + +// Git 内部ファイル(絶対に同期しない) +.git + +// 依存関係・ビルド成果物(大量ファイル、同期不要) +node_modules +.next +out +dist +build + +// 環境変数(秘密情報、意図的にリポジトリ外) +.env +.env.local + +// OS 一時ファイル +.DS_Store +Thumbs.db diff --git a/posimai-dev/server.js b/posimai-dev/server.js index 197cee74..4a4b17aa 100644 --- a/posimai-dev/server.js +++ b/posimai-dev/server.js @@ -228,10 +228,21 @@ app.get('/api/vps-health', async (req, res) => { // ── サービス死活チェックプロキシ (/api/check?url=...) ────────── // ブラウザの mixed-content 制限を回避するためサーバー側から HTTP チェック +// SSRF 対策: http/https のみ許可、クラウドメタデータ IP をブロック +const BLOCKED_HOSTS = /^(169\.254\.|100\.100\.100\.100|metadata\.google\.internal)/; +function isCheckUrlAllowed(raw) { + let parsed; + try { parsed = new URL(raw); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + if (BLOCKED_HOSTS.test(parsed.hostname)) return false; + return true; +} + app.get('/api/check', 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' }); + if (!isCheckUrlAllowed(url)) return res.status(400).json({ ok: false, error: 'url not allowed' }); const t0 = Date.now(); try { const ctrl = new AbortController(); diff --git a/server.js b/server.js index 0dd86baa..b5f058b1 100644 --- a/server.js +++ b/server.js @@ -43,8 +43,21 @@ setInterval(() => { } }, 10 * 60 * 1000); +// ── ユーティリティ ─────────────────────────────────────────────────── +function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + // ── Auth: JWT config ──────────────────────────────────────────────── const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-CHANGE-IN-PRODUCTION'; +if (!process.env.JWT_SECRET) { + console.error('[SECURITY] JWT_SECRET is not set. Using insecure default. Set JWT_SECRET env var in production!'); +} const JWT_TTL_SECONDS = 30 * 24 * 60 * 60; // 30 days // WebAuthn relying party config (from env) @@ -1210,13 +1223,13 @@ function buildRouter() { 保存完了

✓ 保存しました

-

${meta.title}

+

${escapeHtml(meta.title)}

AI分析をバックグラウンドで開始しました

`); } catch (e) { - res.status(500).send(`

保存失敗: ${e.message}

`); + res.status(500).send(`

保存失敗: ${escapeHtml(e.message)}

`); } });