支付回调接口SQL注入风险必然存在,因验签仅保来源可信而不限制参数内容安全,若签名验签不严、参数未过滤或直接拼SQL,攻击者可绕过客户端直击后端并执行恶意SQL。

支付回调接口的SQL注入风险,不是“会不会发生”,而是“攻击者已经绕过客户端直打你后端”——签名验签不严、参数未过滤、直接拼SQL,三者任缺其一,就等于把数据库钥匙挂在回调URL上。
为什么支付宝/微信回调验签失败后还可能被SQL注入
验签只是确认“请求来自支付宝/微信”,不代表参数内容安全。常见错误是:验签通过后,直接把out_trade_no或trade_no拼进SQL查询语句,比如"SELECT * FROM orders WHERE out_trade_no = '" + out_trade_no + "'"。此时攻击者只要伪造一个合法签名(例如通过泄露的appSecret重放+篡改参数),就能在out_trade_no里塞' OR '1'='1。
- 验签和SQL防护必须分两层:验签保来源可信,参数过滤保内容无害
- 微信ApiV3回调的
resource.ciphertext字段解密后仍是JSON字符串,若解析后未校验字段类型就入库,一样会中招 - 支付宝回调中
out_trade_no理论上应为纯字母数字,但攻击者可传ORDER-123'; DROP TABLE orders;--,后端若不做正则清洗就进DB,后果立现
回调参数必须强制白名单校验的字段与规则
不能依赖“验签了就安全”,所有回调字段都要按业务语义做最小化约束。以下字段在支付成功回调中最常被滥用:
-
out_trade_no:必须匹配^[a-zA-Z0-9_-]{8,64}$,且禁止.、%、'、"、;、--、/*等字符;特别注意下划线_和短横-虽常见,但某些老系统用它们做分隔符,反而易被用于绕过 -
total_amount:必须是严格两位小数的数字字符串,用number_format(floatval($amount), 2, '.', '')标准化,再比对原始字符串是否一致;避免传99.999或+99.99触发浮点解析漏洞 -
trade_status:只允许TRADE_SUCCESS、TRADE_CLOSED、WAIT_BUYER_PAY等固定枚举值,禁止任何额外空格、换行或Unicode零宽字符 -
notify_id(支付宝)或id(微信):必须为非空字符串,长度≤128,且不能含控制字符(用ctype_print()或StringUtils.isAsciiPrintable()校验)
Spring Boot处理微信ApiV3回调时如何避免SQL拼接
微信ApiV3回调体是加密JSON,解密后得到明文JSON,但很多开发者解密完直接objectMapper.readValue(jsonStr, Map.class),再取map.get("out_trade_no")去查库——这一步就是高危入口。
- 解密后必须先转成
JsonNode,再用jsonNode.get("resource").get("original_type").asText()逐层取值,避免get("out_trade_no")返回null导致NPE或默认值绕过 - 查订单必须用PreparedStatement:
"SELECT id, status FROM orders WHERE out_trade_no = ? AND app_id = ?",两个?分别绑定out_trade_no和app_id(后者从验签上下文获取,不可信源) - 更新状态时,禁止用
UPDATE orders SET status = '${status}' WHERE ...,必须用setString(1, status)并限定status只能是预定义枚举 - 日志记录回调原始参数时,要脱敏
out_trade_no(如只记前4后4:ORDER-1234****5678),防止审计日志本身成为SQL注入载荷的存储地
最易被忽略的细节:回调重放+时间窗校验没绑死
很多团队实现了验签,也做了参数白名单,但没校验notify_time或create_time的时间有效性。攻击者截获一次成功回调,修改out_trade_no为恶意值,再原样重发——只要签名仍有效,你的服务就照单处理。
- 支付宝
notify_time格式为"2023-08-15 12:00:00",需转为LocalDateTime后判断是否在当前时间±15分钟内 - 微信
create_time是RFC3339格式(如"2015-05-20T13:29:35+08:00"),必须用ZonedDateTime.parse()解析,再检查zdt.isBefore(Instant.now().minusSeconds(300)) - 时间校验必须在验签之后、参数白名单之前执行——否则攻击者可用超时请求耗尽你的时间解析逻辑资源
- 重放检测不能只靠时间戳,要结合
notify_id或微信的id做Redis Set缓存(TTL设为300秒),重复即拒











