fix: security audit - prisma singleton, rate limits, encryption key validation, syntax fixes
This commit is contained in:
61
src/index.ts
61
src/index.ts
@@ -13,6 +13,7 @@ import schemeAobRoutes from './routes/schemesAob';
|
|||||||
import filterRoutes from './routes/filters';
|
import filterRoutes from './routes/filters';
|
||||||
import vipRoutes from './routes/vip';
|
import vipRoutes from './routes/vip';
|
||||||
import favoriteRoutes from './routes/favorites';
|
import favoriteRoutes from './routes/favorites';
|
||||||
|
import { prisma } from './utils/prisma';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
@@ -49,22 +50,31 @@ app.use(cors({
|
|||||||
app.use(express.json({ limit: '10mb' }));
|
app.use(express.json({ limit: '10mb' }));
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
// 速率限制
|
// 速率限制 — 全局
|
||||||
const limiter = rateLimit({
|
const limiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000, // 15 分钟
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 100, // 每个 IP 最多 100 次请求
|
max: 1000,
|
||||||
message: { success: false, message: '请求过于频繁,请稍后再试' },
|
message: { success: false, message: '请求过于频繁,请稍后再试' },
|
||||||
});
|
});
|
||||||
app.use('/api/', limiter);
|
app.use('/api/', limiter);
|
||||||
|
|
||||||
// 登录接口更严格的限制
|
// 敏感端点更严格的限制
|
||||||
const loginLimiter = rateLimit({
|
const strictLimiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 10,
|
max: 10,
|
||||||
message: { success: false, message: '登录尝试过于频繁,请稍后再试' },
|
message: { success: false, message: '操作过于频繁,请稍后再试' },
|
||||||
});
|
});
|
||||||
app.use('/api/login', loginLimiter);
|
app.use('/api/login', strictLimiter);
|
||||||
app.use('/api/register', loginLimiter);
|
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 中间件拦截)
|
// 收藏计数(放在 favorites 路由之前,避免 auth 中间件拦截)
|
||||||
app.get('/api/favorites/count', async (req: Request, res: Response) => {
|
app.get('/api/favorites/count', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { PrismaClient } = require('@prisma/client');
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
const count = await prisma.favorite.count();
|
const count = await prisma.favorite.count();
|
||||||
res.json({ success: true, count });
|
res.json({ success: true, count });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -209,7 +217,9 @@ app.post('/api/adverts/:id/click', (req, res) => {
|
|||||||
// 头像列表
|
// 头像列表
|
||||||
app.get('/api/avatars', (req, res) => {
|
app.get('/api/avatars', (req, res) => {
|
||||||
res.json({ success: true, data: [] });
|
res.json({ success: true, data: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
// 更新服务 (electron-updater)
|
// 更新服务 (electron-updater)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
@@ -234,6 +244,7 @@ releaseDate: '2024-01-01T00:00:00.000Z'
|
|||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 开发工具端点 (需 ADMIN_SECRET)
|
// 开发工具端点 (需 ADMIN_SECRET)
|
||||||
|
// ============================================
|
||||||
app.post('/api/admin/set-vip', async (req, res) => {
|
app.post('/api/admin/set-vip', async (req, res) => {
|
||||||
const secret = process.env.ADMIN_SECRET;
|
const secret = process.env.ADMIN_SECRET;
|
||||||
if (!secret || req.headers['x-admin-secret'] !== secret) {
|
if (!secret || req.headers['x-admin-secret'] !== secret) {
|
||||||
@@ -242,8 +253,6 @@ app.post('/api/admin/set-vip', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { username, isVip } = req.body;
|
const { username, isVip } = req.body;
|
||||||
if (!username) return res.status(400).json({ success: false, message: '缺少 username' });
|
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({
|
const user = await prisma.user.update({
|
||||||
where: { username },
|
where: { username },
|
||||||
data: {
|
data: {
|
||||||
@@ -252,36 +261,12 @@ app.post('/api/admin/set-vip', async (req, res) => {
|
|||||||
vipLevel: isVip !== false ? 1 : 0,
|
vipLevel: isVip !== false ? 1 : 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await prisma.$disconnect();
|
|
||||||
res.json({ success: true, username: user.username, isVip: user.isVip });
|
res.json({ success: true, username: user.username, isVip: user.isVip });
|
||||||
} catch (e: any) {
|
} 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'
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 错误处理
|
// 错误处理
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { verifyToken, extractToken } from '../utils/jwt';
|
import { verifyToken, extractToken } from '../utils/jwt';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { prisma } from '../utils/prisma';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// 扩展 Request 类型
|
// 扩展 Request 类型
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { generateToken } from '../utils/jwt';
|
import { generateToken } from '../utils/jwt';
|
||||||
import { authMiddleware } from '../middleware/auth';
|
import { authMiddleware } from '../middleware/auth';
|
||||||
|
import { prisma } from '../utils/prisma';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 注册
|
// 注册
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { authMiddleware } from '../middleware/auth';
|
import { authMiddleware } from '../middleware/auth';
|
||||||
|
import { prisma } from '../utils/prisma';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { authMiddleware, optionalAuth } from '../middleware/auth';
|
import { authMiddleware, optionalAuth } from '../middleware/auth';
|
||||||
|
import { prisma } from '../utils/prisma';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 获取滤镜列表
|
// 获取滤镜列表
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { authMiddleware, optionalAuth } from '../middleware/auth';
|
import { authMiddleware, optionalAuth } from '../middleware/auth';
|
||||||
import { encrypt } from '../utils/encryption';
|
import { encrypt } from '../utils/encryption';
|
||||||
|
import { prisma } from '../utils/prisma';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 获取方案列表
|
// 获取方案列表
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { authMiddleware, optionalAuth } from '../middleware/auth';
|
import { authMiddleware, optionalAuth } from '../middleware/auth';
|
||||||
import { encrypt } from '../utils/encryption';
|
import { encrypt } from '../utils/encryption';
|
||||||
|
import { prisma } from '../utils/prisma';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 获取全面战场方案列表
|
// 获取全面战场方案列表
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { authMiddleware } from '../middleware/auth';
|
import { authMiddleware } from '../middleware/auth';
|
||||||
|
import { prisma } from '../utils/prisma';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// 所有路由都需要认证
|
// 所有路由都需要认证
|
||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { authMiddleware } from '../middleware/auth';
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { generateToken } from '../utils/jwt';
|
import { generateToken } from '../utils/jwt';
|
||||||
|
import { prisma } from '../utils/prisma';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
function hashCardKey(raw: string): string {
|
function hashCardKey(raw: string): string {
|
||||||
return crypto.createHash('sha256').update(raw).digest('hex');
|
return crypto.createHash('sha256').update(raw).digest('hex');
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
const ALGORITHM = 'aes-256-cbc';
|
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 {
|
export interface EncryptedData {
|
||||||
encrypted: boolean;
|
encrypted: boolean;
|
||||||
@@ -14,9 +22,7 @@ export interface EncryptedData {
|
|||||||
*/
|
*/
|
||||||
export function encrypt(text: string): EncryptedData {
|
export function encrypt(text: string): EncryptedData {
|
||||||
const iv = crypto.randomBytes(16);
|
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');
|
let encrypted = cipher.update(text, 'utf-8', 'hex');
|
||||||
encrypted += cipher.final('hex');
|
encrypted += cipher.final('hex');
|
||||||
|
|
||||||
@@ -33,9 +39,7 @@ export function encrypt(text: string): EncryptedData {
|
|||||||
export function decrypt(ivHex: string, dataHex: string): string {
|
export function decrypt(ivHex: string, dataHex: string): string {
|
||||||
const iv = Buffer.from(ivHex, 'hex');
|
const iv = Buffer.from(ivHex, 'hex');
|
||||||
const encryptedData = Buffer.from(dataHex, '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');
|
let decrypted = decipher.update(encryptedData, undefined, 'utf-8');
|
||||||
decrypted += decipher.final('utf-8');
|
decrypted += decipher.final('utf-8');
|
||||||
|
|
||||||
|
|||||||
7
src/utils/prisma.ts
Normal file
7
src/utils/prisma.ts
Normal 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;
|
||||||
Reference in New Issue
Block a user