fix: HIGH+MEDIUM bugs from full audit - listen error, JWT, upload, mailer, webhook
This commit is contained in:
@@ -42,17 +42,28 @@ function renderTemplate(template, vars) {
|
|||||||
return { subject, body };
|
return { subject, body };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _transporter = null;
|
||||||
|
let _transporterKey = '';
|
||||||
|
|
||||||
|
function getTransporter(cfg) {
|
||||||
|
const key = `${cfg.host}:${cfg.port}:${cfg.user}`;
|
||||||
|
if (_transporter && _transporterKey === key) return _transporter;
|
||||||
|
_transporterKey = key;
|
||||||
|
_transporter = nodemailer.createTransport({ host: cfg.host, port: cfg.port, secure: cfg.secure, auth: { user: cfg.user, pass: cfg.pass } });
|
||||||
|
return _transporter;
|
||||||
|
}
|
||||||
|
|
||||||
async function sendEmail(to, templateCode, vars) {
|
async function sendEmail(to, templateCode, vars) {
|
||||||
try {
|
try {
|
||||||
const cfg = await getSmtpConfig();
|
const cfg = await getSmtpConfig();
|
||||||
if (!cfg.host || !cfg.user) { console.log('[Mailer] SMTP未配置'); return false; }
|
if (!cfg.host || !cfg.user) { console.log('[Mailer] SMTP未配置'); return false; }
|
||||||
const template = await getTemplate(templateCode);
|
const template = await getTemplate(templateCode);
|
||||||
if (!template) { console.log('[Mailer] 模板不存在:', templateCode); return false; }
|
if (!template) { console.log('[Mailer] 模板不存在:', templateCode); return false; }
|
||||||
const allVars = { ...vars, site_name: await getSiteName(), site_url: await getSiteUrl() };
|
const siteName = await getSiteName();
|
||||||
|
const allVars = { ...vars, site_name: siteName, site_url: await getSiteUrl() };
|
||||||
const { subject, body } = renderTemplate(template, allVars);
|
const { subject, body } = renderTemplate(template, allVars);
|
||||||
|
const transporter = getTransporter(cfg);
|
||||||
const transporter = nodemailer.createTransport({ host: cfg.host, port: cfg.port, secure: cfg.secure, auth: { user: cfg.user, pass: cfg.pass } });
|
await transporter.sendMail({ from: `"${siteName}" <${cfg.from}>`, to, subject, html: body });
|
||||||
await transporter.sendMail({ from: `"${await getSiteName()}" <${cfg.from}>`, to, subject, html: body });
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) { console.error('[Mailer]', err.message); return false; }
|
} catch (err) { console.error('[Mailer]', err.message); return false; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,13 @@ const { getConfig } = require('../db');
|
|||||||
function getSecret() {
|
function getSecret() {
|
||||||
try {
|
try {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
return cfg?.jwt_secret || process.env.JWT_SECRET || 'mc-dev-secret';
|
const secret = cfg?.jwt_secret || process.env.JWT_SECRET;
|
||||||
} catch { return process.env.JWT_SECRET || 'mc-dev-secret'; }
|
if (secret) return secret;
|
||||||
|
throw new Error('JWT_SECRET not configured');
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === 'JWT_SECRET not configured') throw e;
|
||||||
|
return process.env.JWT_SECRET || (() => { throw new Error('JWT_SECRET not configured'); })();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateToken(user) {
|
function generateToken(user) {
|
||||||
|
|||||||
@@ -33,13 +33,14 @@ function sanitize(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeBody(req, res, next) {
|
function sanitizeBody(req, res, next) {
|
||||||
if (req.body) {
|
function walk(obj) {
|
||||||
for (const key of Object.keys(req.body)) {
|
if (!obj || typeof obj !== 'object') return;
|
||||||
if (typeof req.body[key] === 'string') {
|
for (const key of Object.keys(obj)) {
|
||||||
req.body[key] = sanitize(req.body[key]);
|
if (typeof obj[key] === 'string') obj[key] = sanitize(obj[key]);
|
||||||
}
|
else if (typeof obj[key] === 'object') walk(obj[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
walk(req.body);
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function validateMagicBytes(buffer, mime) {
|
|||||||
'image/png': () => head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4E && head[3] === 0x47,
|
'image/png': () => head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4E && head[3] === 0x47,
|
||||||
'image/gif': () => head.toString('ascii', 0, 6) === 'GIF89a' || head.toString('ascii', 0, 6) === 'GIF87a',
|
'image/gif': () => head.toString('ascii', 0, 6) === 'GIF89a' || head.toString('ascii', 0, 6) === 'GIF87a',
|
||||||
'image/webp': () => head.toString('ascii', 0, 4) === 'RIFF' && head.toString('ascii', 8, 12) === 'WEBP',
|
'image/webp': () => head.toString('ascii', 0, 4) === 'RIFF' && head.toString('ascii', 8, 12) === 'WEBP',
|
||||||
'video/mp4': () => buffer.includes(Buffer.from('ftyp')),
|
'video/mp4': () => buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70,
|
||||||
'video/webm': () => head[0] === 0x1A && head[1] === 0x45 && head[2] === 0xDF && head[3] === 0xA3,
|
'video/webm': () => head[0] === 0x1A && head[1] === 0x45 && head[2] === 0xDF && head[3] === 0xA3,
|
||||||
};
|
};
|
||||||
if (!sigs[mime]) return false;
|
if (!sigs[mime]) return false;
|
||||||
@@ -58,18 +58,22 @@ const upload = multer({
|
|||||||
|
|
||||||
function finalizeUpload(req, res, next) {
|
function finalizeUpload(req, res, next) {
|
||||||
if (!req.files || req.files.length === 0) return next();
|
if (!req.files || req.files.length === 0) return next();
|
||||||
for (const file of req.files) {
|
const filePaths = [];
|
||||||
const buf = fs.readFileSync(file.path);
|
try {
|
||||||
if (!ALLOWED_MIME[file.mimetype]) {
|
for (const file of req.files) {
|
||||||
fs.unlinkSync(file.path);
|
const fd = fs.openSync(file.path, 'r');
|
||||||
return res.status(400).json({ error: `不支持的文件类型: ${file.mimetype}` });
|
const buf = Buffer.alloc(256);
|
||||||
}
|
fs.readSync(fd, buf, 0, 256, 0);
|
||||||
if (!validateMagicBytes(buf, file.mimetype)) {
|
fs.closeSync(fd);
|
||||||
fs.unlinkSync(file.path);
|
filePaths.push(file.path);
|
||||||
return res.status(400).json({ error: '文件内容与声明类型不符,可能是恶意文件' });
|
if (!ALLOWED_MIME[file.mimetype]) throw new Error(`不支持的文件类型: ${file.mimetype}`);
|
||||||
|
if (!validateMagicBytes(buf, file.mimetype)) throw new Error('文件内容与声明类型不符,可能是恶意文件');
|
||||||
}
|
}
|
||||||
|
next();
|
||||||
|
} catch (e) {
|
||||||
|
for (const fp of filePaths) { try { fs.unlinkSync(fp); } catch {} }
|
||||||
|
return res.status(400).json({ error: e.message || '文件校验失败' });
|
||||||
}
|
}
|
||||||
next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { upload, finalizeUpload, validateMagicBytes, UPLOAD_DIR, ALLOWED_MIME };
|
module.exports = { upload, finalizeUpload, validateMagicBytes, UPLOAD_DIR, ALLOWED_MIME };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const { v4: uuid } = require('uuid');
|
const { v4: uuid } = require('uuid');
|
||||||
const { query, getRow, getConfig } = require('../db');
|
const { query, getRow, getConfig, getPool } = require('../db');
|
||||||
const { generateToken, authenticate } = require('../middleware/auth');
|
const { generateToken, authenticate } = require('../middleware/auth');
|
||||||
const { sendEmail } = require('../mailer');
|
const { sendEmail } = require('../mailer');
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ router.post('/auth/login', async (req, res) => {
|
|||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
if (!username || !password) return res.status(400).json({ error: '请输入用户名和密码' });
|
if (!username || !password) return res.status(400).json({ error: '请输入用户名和密码' });
|
||||||
const user = await getRow('SELECT * FROM users WHERE username = ?', [username]);
|
const user = await getRow('SELECT * FROM users WHERE username = ?', [username]);
|
||||||
if (!user || !bcrypt.compareSync(password, user.password)) return res.status(401).json({ error: '用户名或密码错误' });
|
if (!user || !(await bcrypt.compare(password, user.password))) return res.status(401).json({ error: '用户名或密码错误' });
|
||||||
if (!user.active) return res.status(403).json({ error: '账号未激活' });
|
if (!user.active) return res.status(403).json({ error: '账号未激活' });
|
||||||
const token = generateToken(user);
|
const token = generateToken(user);
|
||||||
res.json({ token, user: { id:user.id, username:user.username, game_name:user.game_name, game_uid:user.game_uid, role:user.role } });
|
res.json({ token, user: { id:user.id, username:user.username, game_name:user.game_name, game_uid:user.game_uid, role:user.role } });
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ router.put('/:id', authenticate, requireRole('owner','admin'), async (req, res)
|
|||||||
const fields = {};
|
const fields = {};
|
||||||
if (req.body.status) fields.status = req.body.status;
|
if (req.body.status) fields.status = req.body.status;
|
||||||
if (req.body.priority) fields.priority = req.body.priority;
|
if (req.body.priority) fields.priority = req.body.priority;
|
||||||
|
if (req.body.priority && !['low','medium','high','urgent'].includes(req.body.priority)) return res.status(400).json({ error: '无效的优先级' });
|
||||||
if (req.body.claim_note !== undefined) fields.claim_note = req.body.claim_note;
|
if (req.body.claim_note !== undefined) fields.claim_note = req.body.claim_note;
|
||||||
if (!Object.keys(fields).length) return res.status(400).json({ error: '无更新内容' });
|
if (!Object.keys(fields).length) return res.status(400).json({ error: '无更新内容' });
|
||||||
const sets = Object.keys(fields).map(k => `${k} = ?`).join(', ');
|
const sets = Object.keys(fields).map(k => `${k} = ?`).join(', ');
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ app.use((err, req, res, next) => {
|
|||||||
res.status(500).json({ error: '服务器内部错误' });
|
res.status(500).json({ error: '服务器内部错误' });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isInstalled()) console.log(`Server running at http://localhost:${PORT}`);
|
app.listen(PORT, () => {
|
||||||
else console.log(`System not installed. Visit http://localhost:${PORT}/#/install`);
|
if (isInstalled()) console.log(`Server running at http://localhost:${PORT}`);
|
||||||
|
else console.log(`System not installed. Visit http://localhost:${PORT}/#/install`);
|
||||||
app.listen(PORT);
|
}).on('error', (err) => { console.error('Failed to start:', err.message); process.exit(1); });
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ async function sendWebhook(event, data) {
|
|||||||
const events = cfg.events || 'all';
|
const events = cfg.events || 'all';
|
||||||
if (events !== 'all' && !events.split(',').map(s=>s.trim()).includes(event)) continue;
|
if (events !== 'all' && !events.split(',').map(s=>s.trim()).includes(event)) continue;
|
||||||
const embed = buildEmbed(event, data);
|
const embed = buildEmbed(event, data);
|
||||||
try { await fetch(cfg.webhook_url, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(embed) }); } catch {}
|
try {
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
const t = setTimeout(() => ctrl.abort(), 10000);
|
||||||
|
await fetch(cfg.webhook_url, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(embed), signal: ctrl.signal });
|
||||||
|
clearTimeout(t);
|
||||||
|
} catch (e) { console.error('[Webhook]', cfg.name, e.message); }
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user