fix: 全面安全审计修复 — 支付验签、IDOR、竞态、token过期等
- H1: 支付宝响应验签 (verifyResponseSign + bracket-matching 提取签名内容) - H2/H3: EasyPay queryOrder 从 GET 改 POST,PKEY 不再暴露于 URL - H5: users/[id] IDOR 修复,校验当前用户只能查询自身信息 - H6: 限额校验移入 prisma.$transaction() 防止 TOCTOU 竞态 - C1: access_token 增加 24h 过期、userId 绑定、派生密钥分离 - M1: EasyPay 回调增加 pid 校验防跨商户注入 - M4: 充值码增加 crypto.randomBytes 随机后缀 - M5: 过期订单批量处理增加 BATCH_SIZE 限制 - M6: 退款失败增加 [CRITICAL] 日志和余额补偿标记 - M7: admin channels PUT 增加 Zod schema 校验 - M8: admin subscriptions 分页参数增加上限 - M9: orders src_url 限制 HTTP/HTTPS 协议 - L1: 微信支付回调时间戳 NaN 检查 - L9: WXPAY_API_V3_KEY 长度校验
This commit is contained in:
@@ -1,13 +1,33 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth';
|
||||
import { prisma } from '@/lib/db';
|
||||
|
||||
const updateChannelSchema = z
|
||||
.object({
|
||||
group_id: z.number().int().positive().optional(),
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
platform: z.string().min(1).max(50).optional(),
|
||||
rate_multiplier: z.number().positive().optional(),
|
||||
description: z.string().max(500).nullable().optional(),
|
||||
models: z.array(z.string()).nullable().optional(),
|
||||
features: z.record(z.string(), z.unknown()).nullable().optional(),
|
||||
sort_order: z.number().int().min(0).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
if (!(await verifyAdminToken(request))) return unauthorizedResponse(request);
|
||||
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const rawBody = await request.json();
|
||||
const parsed = updateChannelSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: '参数校验失败' }, { status: 400 });
|
||||
}
|
||||
const body = parsed.data;
|
||||
|
||||
const existing = await prisma.channel.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
@@ -27,15 +47,8 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
if (body.rate_multiplier !== undefined && (typeof body.rate_multiplier !== 'number' || body.rate_multiplier <= 0)) {
|
||||
return NextResponse.json({ error: 'rate_multiplier 必须是正数' }, { status: 400 });
|
||||
}
|
||||
if (body.sort_order !== undefined && (!Number.isInteger(body.sort_order) || body.sort_order < 0)) {
|
||||
return NextResponse.json({ error: 'sort_order 必须是非负整数' }, { status: 400 });
|
||||
}
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
if (body.group_id !== undefined) data.groupId = Number(body.group_id);
|
||||
if (body.group_id !== undefined) data.groupId = body.group_id;
|
||||
if (body.name !== undefined) data.name = body.name;
|
||||
if (body.platform !== undefined) data.platform = body.platform;
|
||||
if (body.rate_multiplier !== undefined) data.rateMultiplier = body.rate_multiplier;
|
||||
|
||||
@@ -37,8 +37,8 @@ export async function GET(request: NextRequest) {
|
||||
const result = await listSubscriptions({
|
||||
group_id: groupId ? Number(groupId) : undefined,
|
||||
status: status || undefined,
|
||||
page: page ? Number(page) : undefined,
|
||||
page_size: pageSize ? Number(pageSize) : undefined,
|
||||
page: page ? Math.max(1, Number(page)) : undefined,
|
||||
page_size: pageSize ? Math.min(200, Math.max(1, Number(pageSize))) : undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
Reference in New Issue
Block a user