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
|
.vercel
|
||||||
|
|
||||||
|
# Syncthing コンフリクトファイル
|
||||||
|
*.sync-conflict-*
|
||||||
|
|
||||||
# 一時ファイル・バックアップ(ルート)
|
# 一時ファイル・バックアップ(ルート)
|
||||||
_tmp_*
|
_tmp_*
|
||||||
*.backup*
|
*.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=...) ──────────
|
// ── サービス死活チェックプロキシ (/api/check?url=...) ──────────
|
||||||
// ブラウザの mixed-content 制限を回避するためサーバー側から HTTP チェック
|
// ブラウザの 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) => {
|
app.get('/api/check', async (req, res) => {
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
const { url } = req.query;
|
const { url } = req.query;
|
||||||
if (!url) return res.status(400).json({ ok: false, error: 'url required' });
|
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();
|
const t0 = Date.now();
|
||||||
try {
|
try {
|
||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
|
|
|
||||||
17
server.js
17
server.js
|
|
@ -43,8 +43,21 @@ setInterval(() => {
|
||||||
}
|
}
|
||||||
}, 10 * 60 * 1000);
|
}, 10 * 60 * 1000);
|
||||||
|
|
||||||
|
// ── ユーティリティ ───────────────────────────────────────────────────
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
// ── Auth: JWT config ────────────────────────────────────────────────
|
// ── Auth: JWT config ────────────────────────────────────────────────
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-CHANGE-IN-PRODUCTION';
|
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
|
const JWT_TTL_SECONDS = 30 * 24 * 60 * 60; // 30 days
|
||||||
|
|
||||||
// WebAuthn relying party config (from env)
|
// WebAuthn relying party config (from env)
|
||||||
|
|
@ -1210,13 +1223,13 @@ function buildRouter() {
|
||||||
<html><head><meta charset="utf-8"><title>保存完了</title></head>
|
<html><head><meta charset="utf-8"><title>保存完了</title></head>
|
||||||
<body style="font-family:sans-serif;padding:40px;text-align:center;background:#0a0a0a;color:#e2e2e2">
|
<body style="font-family:sans-serif;padding:40px;text-align:center;background:#0a0a0a;color:#e2e2e2">
|
||||||
<h1 style="color:#818CF8">✓ 保存しました</h1>
|
<h1 style="color:#818CF8">✓ 保存しました</h1>
|
||||||
<p>${meta.title}</p>
|
<p>${escapeHtml(meta.title)}</p>
|
||||||
<p style="color:#888">AI分析をバックグラウンドで開始しました</p>
|
<p style="color:#888">AI分析をバックグラウンドで開始しました</p>
|
||||||
<script>setTimeout(() => window.close(), 1500)</script>
|
<script>setTimeout(() => window.close(), 1500)</script>
|
||||||
</body></html>
|
</body></html>
|
||||||
`);
|
`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(`<h1>保存失敗: ${e.message}</h1>`);
|
res.status(500).send(`<h1>保存失敗: ${escapeHtml(e.message)}</h1>`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue