fix: security audit - prisma singleton, rate limits, encryption key validation, syntax fixes

This commit is contained in:
2026-05-11 23:40:44 +08:00
parent 3044b3d921
commit 9b924d5e5d
11 changed files with 49 additions and 62 deletions

View File

@@ -13,6 +13,7 @@ import schemeAobRoutes from './routes/schemesAob';
import filterRoutes from './routes/filters';
import vipRoutes from './routes/vip';
import favoriteRoutes from './routes/favorites';
import { prisma } from './utils/prisma';
const app = express();
const PORT = process.env.PORT || 3001;
@@ -49,22 +50,31 @@ app.use(cors({
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// 速率限制
// 速率限制 — 全局
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每个 IP 最多 100 次请求
windowMs: 15 * 60 * 1000,
max: 1000,
message: { success: false, message: '请求过于频繁,请稍后再试' },
});
app.use('/api/', limiter);
// 登录接口更严格的限制
const loginLimiter = rateLimit({
// 敏感端点更严格的限制
const strictLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
message: { success: false, message: '登录尝试过于频繁,请稍后再试' },
message: { success: false, message: '操作过于频繁,请稍后再试' },
});
app.use('/api/login', loginLimiter);
app.use('/api/register', loginLimiter);
app.use('/api/login', strictLimiter);
app.use('/api/register', strictLimiter);
// 会话轮询限流 (防止高频 session-status 刷库)
const pollLimiter = rateLimit({
windowMs: 60 * 1000,
max: 60,
message: { success: false, message: '轮询过于频繁' },
});
app.use('/api/session-status', pollLimiter);
app.use('/api/vip-status', pollLimiter);
// ============================================
// 路由
@@ -94,8 +104,6 @@ app.use('/api', vipRoutes);
// 收藏计数(放在 favorites 路由之前,避免 auth 中间件拦截)
app.get('/api/favorites/count', async (req: Request, res: Response) => {
try {
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const count = await prisma.favorite.count();
res.json({ success: true, count });
} catch (e) {
@@ -209,7 +217,9 @@ app.post('/api/adverts/:id/click', (req, res) => {
// 头像列表
app.get('/api/avatars', (req, res) => {
res.json({ success: true, data: [] });
});
// ============================================
// 更新服务 (electron-updater)
// ============================================
@@ -234,6 +244,7 @@ releaseDate: '2024-01-01T00:00:00.000Z'
// ============================================
// 开发工具端点 (需 ADMIN_SECRET)
// ============================================
app.post('/api/admin/set-vip', async (req, res) => {
const secret = process.env.ADMIN_SECRET;
if (!secret || req.headers['x-admin-secret'] !== secret) {
@@ -242,8 +253,6 @@ app.post('/api/admin/set-vip', async (req, res) => {
try {
const { username, isVip } = req.body;
if (!username) return res.status(400).json({ success: false, message: '缺少 username' });
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const user = await prisma.user.update({
where: { username },
data: {
@@ -252,36 +261,12 @@ app.post('/api/admin/set-vip', async (req, res) => {
vipLevel: isVip !== false ? 1 : 0,
},
});
await prisma.$disconnect();
res.json({ success: true, username: user.username, isVip: user.isVip });
} catch (e: any) {
res.status(500).json({ success: false, message: e.message });
console.error('Admin set-vip error:', e);
res.status(500).json({ success: false, message: '操作失败' });
}
});
});
// ============================================
// 更新服务 (electron-updater)
// ============================================
// 更新配置
app.get('/update-config.json', (req, res) => {
res.json({
version: '7.0.4',
url: '',
notes: '',
mandatory: false,
});
});
// latest.yml (electron-updater 标准格式)
app.get('/latest.yml', (req, res) => {
res.type('text/yaml');
res.send(`version: 7.0.4
files: []
releaseDate: '2024-01-01T00:00:00.000Z'
`);
});
// ============================================
// 错误处理

View File

@@ -1,8 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { verifyToken, extractToken } from '../utils/jwt';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
import { prisma } from '../utils/prisma';
// 扩展 Request 类型
declare global {

View File

@@ -1,12 +1,11 @@
import { Router, Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { generateToken } from '../utils/jwt';
import { authMiddleware } from '../middleware/auth';
import { prisma } from '../utils/prisma';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// 注册

View File

@@ -1,10 +1,9 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { authMiddleware } from '../middleware/auth';
import { prisma } from '../utils/prisma';
const router = Router();
const prisma = new PrismaClient();
router.use(authMiddleware);

View File

@@ -1,9 +1,8 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { authMiddleware, optionalAuth } from '../middleware/auth';
import { prisma } from '../utils/prisma';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// 获取滤镜列表

View File

@@ -1,11 +1,10 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { authMiddleware, optionalAuth } from '../middleware/auth';
import { encrypt } from '../utils/encryption';
import { prisma } from '../utils/prisma';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// 获取方案列表

View File

@@ -1,10 +1,9 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { authMiddleware, optionalAuth } from '../middleware/auth';
import { encrypt } from '../utils/encryption';
import { prisma } from '../utils/prisma';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// 获取全面战场方案列表

View File

@@ -1,10 +1,9 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { authMiddleware } from '../middleware/auth';
import { prisma } from '../utils/prisma';
const router = Router();
const prisma = new PrismaClient();
// 所有路由都需要认证
router.use(authMiddleware);

View File

@@ -1,12 +1,11 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import crypto from 'crypto';
import { z } from 'zod';
import { authMiddleware } from '../middleware/auth';
import { generateToken } from '../utils/jwt';
import { prisma } from '../utils/prisma';
const router = Router();
const prisma = new PrismaClient();
function hashCardKey(raw: string): string {
return crypto.createHash('sha256').update(raw).digest('hex');

View File

@@ -1,7 +1,15 @@
import crypto from 'crypto';
const ALGORITHM = 'aes-256-cbc';
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || '0123456789abcdef0123456789abcdef';
const KEY = (() => {
const raw = process.env.ENCRYPTION_KEY || '0123456789abcdef0123456789abcdef';
const buf = Buffer.from(raw, 'utf-8');
if (buf.length !== 32) {
console.error(`ENCRYPTION_KEY must be exactly 32 bytes, got ${buf.length}`);
process.exit(1);
}
return buf;
})();
export interface EncryptedData {
encrypted: boolean;
@@ -14,9 +22,7 @@ export interface EncryptedData {
*/
export function encrypt(text: string): EncryptedData {
const iv = crypto.randomBytes(16);
const key = Buffer.from(ENCRYPTION_KEY, 'utf-8');
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
let encrypted = cipher.update(text, 'utf-8', 'hex');
encrypted += cipher.final('hex');
@@ -33,9 +39,7 @@ export function encrypt(text: string): EncryptedData {
export function decrypt(ivHex: string, dataHex: string): string {
const iv = Buffer.from(ivHex, 'hex');
const encryptedData = Buffer.from(dataHex, 'hex');
const key = Buffer.from(ENCRYPTION_KEY, 'utf-8');
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv);
let decrypted = decipher.update(encryptedData, undefined, 'utf-8');
decrypted += decipher.final('utf-8');

7
src/utils/prisma.ts Normal file
View File

@@ -0,0 +1,7 @@
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;