From 0c65ef95f8684ef471fec4de552ea9b65d4e31da Mon Sep 17 00:00:00 2001 From: Guoguo Date: Mon, 11 May 2026 06:22:24 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20cross-audit=20fixes=20=E2=80=94=20file?= =?UTF-8?q?=20validation,=20type=20safety,=20dedup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Avatar upload: whitelist image MIME types and extensions (jpg/png/gif/webp) - Normalize device_id to String for strict comparison in treatment sync - Add ORDER BY expire_time DESC to purchase/adminCreate subscription queries - Deduplicate readBearer: middleware imports from lib/auth.js - Parameterize createTrial INTERVAL instead of string concatenation - Add rate limiting (20/15min) to avatar upload and phone auth endpoints --- server/src/app.js | 11 ++++++++++- server/src/dao/subscription.dao.js | 10 +++++----- server/src/middleware/auth.js | 7 +------ server/src/routes/treatment.js | 2 +- server/src/routes/user.js | 15 ++++++++++++--- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/server/src/app.js b/server/src/app.js index 511b4df..76fb93e 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -14,12 +14,19 @@ const userLoginLimiter = rateLimit({ message: { code: 2001, message: 'too_many_attempts' } }) const adminLoginLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes + windowMs: 15 * 60 * 1000, max: 5, standardHeaders: true, legacyHeaders: false, message: { code: 2001, message: 'too_many_attempts' } }) +const uploadLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 20, + standardHeaders: true, + legacyHeaders: false, + message: { code: 2001, message: 'too_many_attempts' } +}) app.use(express.json()) app.use((req, res, next) => { @@ -32,6 +39,8 @@ app.use((req, res, next) => { app.use('/api/v1/auth/login', userLoginLimiter) app.use('/api/v1/admin/login', adminLoginLimiter) +app.use('/api/v1/user/avatar', uploadLimiter) +app.use('/api/v1/user/phone', uploadLimiter) app.use(authMiddleware) diff --git a/server/src/dao/subscription.dao.js b/server/src/dao/subscription.dao.js index 5868b13..1c6335c 100644 --- a/server/src/dao/subscription.dao.js +++ b/server/src/dao/subscription.dao.js @@ -56,10 +56,10 @@ async function findAnyActive(userId) { * @returns {Promise} query result */ async function createTrial(userId, orderId, days) { - const trialDays = days || 7 + const trialDays = Number(days) || 7 return query( - "INSERT INTO subscriptions (user_id, plan, status, amount, order_id, start_time, expire_time) VALUES (:user_id, 'trial', 1, 0, :order_id, NOW(), DATE_ADD(NOW(), INTERVAL " + Number(trialDays) + " DAY))", - { user_id: userId, order_id: orderId || 'TRIAL' + Date.now() } + "INSERT INTO subscriptions (user_id, plan, status, amount, order_id, start_time, expire_time) VALUES (:user_id, 'trial', 1, 0, :order_id, NOW(), DATE_ADD(NOW(), INTERVAL :trial_days DAY))", + { user_id: userId, order_id: orderId || 'TRIAL' + Date.now(), trial_days: trialDays } ) } @@ -77,7 +77,7 @@ async function createTrial(userId, orderId, days) { async function purchase(userId, plan, amount, orderId, days) { return transaction(async conn => { const [rows] = await conn.execute( - 'SELECT subscription_id FROM subscriptions WHERE user_id = ? AND status = 1 AND expire_time > NOW() LIMIT 1', + 'SELECT subscription_id FROM subscriptions WHERE user_id = ? AND status = 1 AND expire_time > NOW() ORDER BY expire_time DESC LIMIT 1', [userId] ) if (rows.length > 0) { @@ -114,7 +114,7 @@ async function purchase(userId, plan, amount, orderId, days) { async function adminCreate(userId, plan, amount, orderId, days) { return transaction(async conn => { const [rows] = await conn.execute( - 'SELECT subscription_id FROM subscriptions WHERE user_id = ? AND status = 1 AND expire_time > NOW() LIMIT 1', + 'SELECT subscription_id FROM subscriptions WHERE user_id = ? AND status = 1 AND expire_time > NOW() ORDER BY expire_time DESC LIMIT 1', [userId] ) if (rows.length > 0) { diff --git a/server/src/middleware/auth.js b/server/src/middleware/auth.js index 8015eeb..8689d8a 100644 --- a/server/src/middleware/auth.js +++ b/server/src/middleware/auth.js @@ -1,12 +1,7 @@ const jwt = require('jsonwebtoken') const config = require('../config') const { one } = require('../lib/db') - -function readBearer(headers) { - const auth = headers.authorization || '' - const match = auth.match(/^Bearer\s+(.+)$/i) - return match ? match[1] : '' -} +const { readBearer } = require('../lib/auth') function authMiddleware(req, res, next) { req.ip = req.headers['x-forwarded-for'] diff --git a/server/src/routes/treatment.js b/server/src/routes/treatment.js index 5a6cf44..28b63f8 100644 --- a/server/src/routes/treatment.js +++ b/server/src/routes/treatment.js @@ -21,7 +21,7 @@ router.post('/treatment/sync', requireUser, wrap(async (req, res) => { if (!d.device_id) return res.json(fail(2001, 'device_id required')) const active = await bindingDao.findActiveByUser(req.user.user_id) - if (!active || active.device_id !== d.device_id) return res.json(fail(1006, 'device_not_bound')) + if (!active || String(active.device_id) !== String(d.device_id)) return res.json(fail(1006, 'device_not_bound')) const sessionId = d.session_id || 'SESS' + Date.now() await treatmentDao.create({ diff --git a/server/src/routes/user.js b/server/src/routes/user.js index 3ea0518..fb95f52 100644 --- a/server/src/routes/user.js +++ b/server/src/routes/user.js @@ -9,7 +9,14 @@ const userDao = require('../dao/user.dao') const deviceDao = require('../dao/device.dao') const logDao = require('../dao/log.dao') -const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 2 * 1024 * 1024 } }) +const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'] +const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: 2 * 1024 * 1024 }, + fileFilter: (req, file, cb) => { + cb(null, ALLOWED_IMAGE_TYPES.includes(file.mimetype)) + } +}) const wrap = fn => (req, res, next) => fn(req, res, next).catch(next) router.get('/user/profile', requireUser, wrap(async (req, res) => { @@ -37,8 +44,10 @@ router.put('/user/profile', requireUser, wrap(async (req, res) => { })) router.post('/user/avatar', requireUser, upload.single('file'), wrap(async (req, res) => { - if (!req.file) return res.json(fail(2001, 'file required')) - const ext = (req.file.originalname || '').split('.').pop() || 'jpg' + if (!req.file) return res.json(fail(2001, 'file required, only jpg/png/gif/webp allowed')) + const ALLOWED_EXTS = ['jpg', 'jpeg', 'png', 'gif', 'webp'] + const ext = (req.file.originalname || '').split('.').pop().toLowerCase().replace(/[^a-z]/g, '') || 'jpg' + if (!ALLOWED_EXTS.includes(ext)) return res.json(fail(2001, 'unsupported image format')) const key = 'avatars/' + req.user.user_id + '_' + Date.now() + '.' + ext const cos = new COS({ SecretId: config.cos.secretId, SecretKey: config.cos.secretKey }) await new Promise((resolve, reject) => {