fix: cross-audit fixes — file validation, type safety, dedup
- 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
这个提交包含在:
+10
-1
@@ -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)
|
||||
|
||||
|
||||
@@ -56,10 +56,10 @@ async function findAnyActive(userId) {
|
||||
* @returns {Promise<Array>} 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) {
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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({
|
||||
|
||||
+12
-3
@@ -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) => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户