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:
erio
2026-03-14 04:36:33 +08:00
parent 34ad876626
commit 4ce3484179
19 changed files with 320 additions and 124 deletions

View File

@@ -43,6 +43,73 @@ export function generateSign(params: Record<string, string>, privateKey: string)
return signer.sign(formatPrivateKey(privateKey), 'base64');
}
/**
* 验证支付宝服务端 API 响应签名。
* 从原始 JSON 文本中提取 responseKey 对应的子串作为验签内容。
*/
export function verifyResponseSign(
rawText: string,
responseKey: string,
alipayPublicKey: string,
sign: string,
): boolean {
// 从原始文本中精确提取 responseKey 对应的 JSON 子串
// 格式: {"responseKey":{ ... },"sign":"..."}
const keyPattern = `"${responseKey}"`;
const keyIdx = rawText.indexOf(keyPattern);
if (keyIdx < 0) return false;
const colonIdx = rawText.indexOf(':', keyIdx + keyPattern.length);
if (colonIdx < 0) return false;
// 找到 value 的起始位置(跳过冒号后的空白)
let start = colonIdx + 1;
while (start < rawText.length && rawText[start] === ' ') start++;
// 使用括号匹配找到完整的 JSON 值
let depth = 0;
let end = start;
let inString = false;
let escaped = false;
for (let i = start; i < rawText.length; i++) {
const ch = rawText[i];
if (escaped) {
escaped = false;
continue;
}
if (ch === '\\' && inString) {
escaped = true;
continue;
}
if (ch === '"') {
inString = !inString;
continue;
}
if (inString) continue;
if (ch === '{') depth++;
if (ch === '}') {
depth--;
if (depth === 0) {
end = i + 1;
break;
}
}
}
const signContent = rawText.substring(start, end);
const pem = formatPublicKey(alipayPublicKey);
try {
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(signContent);
return verifier.verify(pem, sign, 'base64');
} catch (err) {
if (shouldLogVerifyDebug()) {
console.error('[Alipay verifyResponseSign] crypto error:', err);
}
return false;
}
}
/** 用支付宝公钥验证签名(回调验签:排除 sign 和 sign_type */
export function verifySign(params: Record<string, string>, alipayPublicKey: string, sign: string): boolean {
const filtered = Object.entries(params)