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 <noreply@anthropic.com>
This commit is contained in:
posimai 2026-04-03 08:15:45 +09:00
parent 4bd098251f
commit 9e6a2987ed
4 changed files with 49 additions and 2 deletions

3
.gitignore vendored
View File

@ -1,5 +1,8 @@
.vercel
# Syncthing コンフリクトファイル
*.sync-conflict-*
# 一時ファイル・バックアップ(ルート)
_tmp_*
*.backup*

20
.stignore Normal file
View File

@ -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

View File

@ -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();

View File

@ -43,8 +43,21 @@ setInterval(() => {
}
}, 10 * 60 * 1000);
// ── ユーティリティ ───────────────────────────────────────────────────
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}
// ── 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() {
<html><head><meta charset="utf-8"><title>保存完了</title></head>
<body style="font-family:sans-serif;padding:40px;text-align:center;background:#0a0a0a;color:#e2e2e2">
<h1 style="color:#818CF8"> 保存しました</h1>
<p>${meta.title}</p>
<p>${escapeHtml(meta.title)}</p>
<p style="color:#888">AI分析をバックグラウンドで開始しました</p>
<script>setTimeout(() => window.close(), 1500)</script>
</body></html>
`);
} catch (e) {
res.status(500).send(`<h1>保存失敗: ${e.message}</h1>`);
res.status(500).send(`<h1>保存失敗: ${escapeHtml(e.message)}</h1>`);
}
});