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

`); } });