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:
parent
4bd098251f
commit
9e6a2987ed
|
|
@ -1,5 +1,8 @@
|
|||
.vercel
|
||||
|
||||
# Syncthing コンフリクトファイル
|
||||
*.sync-conflict-*
|
||||
|
||||
# 一時ファイル・バックアップ(ルート)
|
||||
_tmp_*
|
||||
*.backup*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
17
server.js
17
server.js
|
|
@ -43,8 +43,21 @@ setInterval(() => {
|
|||
}
|
||||
}, 10 * 60 * 1000);
|
||||
|
||||
// ── ユーティリティ ───────────────────────────────────────────────────
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.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() {
|
|||
<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>`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue