说一个大家可能不太愿意面对的事实:支付回调接口这地方的SQL注入,从来不是什么虚拟威胁——它是切实存在的,而且远比你想象中常见。验签通过,不代表参数安全,这两件事绝对不能画等号。像out_trade_no、total_amount、trade_status这些字段,一旦被直接拼进SQL语句,攻击者就能用 ' OR '1'='1 这类payload,轻松绕过客户端,直捣数据库。

验签之后,为什么还会被注入?
微信或支付宝的回调验签,它的职责很明确:确认请求确实来自官方服务器。但它压根不管参数里的内容是否安全。很多团队会犯的一个错误是,验签一通过,就立刻把 $_POST['out_trade_no'] 直接塞进 "SELECT * FROM orders WHERE out_trade_no = '$out_trade_no'" 这种语句里。这时候,攻击者只要伪造一个合法的签名——比如通过泄露的 app_secret 重放请求并篡改参数——就能在 out_trade_no 中埋入恶意字符串。
正确的做法,是对几个关键字段实施严格的白名单校验:
out_trade_no:必须用正则/^[a-zA-Z0-9_-]{8,64}$/卡死,直接拒绝任何包含'、"、;、--、/*、%、.等符号的输入。下划线和短横虽然是业务常用字符,但某些老系统会用它们来分隔字段,反而可能成为被利用的绕过点,这点得特别注意。total_amount:必须是严格的两位小数字符串。先通过floatval()转换成浮点数,再用number_format($f, 2, '.', '')做标准化,最后比对标准化后的结果和原始输入是否一致。这样可以有效防范+99.99或99.999这类容易触发浮点解析漏洞的写法。trade_status:只允许几个固定枚举值:TRADE_SUCCESS、TRADE_CLOSED、WAIT_BUYER_PAY。必须用in_array()做严格校验,任何肉眼看不见的空格、换行、Unicode零宽字符,一概拒绝。
resource.ciphertext 解密后,仍然是个危险源
微信API v3 返回的 resource.ciphertext,解密后是一段JSON字符串。如果解密出来之后,直接 json_decode($raw, true) 然后不做任何类型校验就入库,那跟敞开门让人打没什么区别。想象一下,攻击者让解密结果里的 "amount" 值变成了 "99.99'; DROP TABLE orders;--",后面只要一拼到SQL里,后果不言自明。
- 解密之后,必须对每个字段再做一次白名单校验。金额字段强制转成
float再格式化;订单号字段再跑一遍preg_match(),确保万无一失。 - 杜绝使用
extract()或者自动映射的方式,把解密结果直接赋给变量。一定要显式取键:$data['out_trade_no'] ?? null,并且每个键都单独走一遍校验流程。 - 强烈建议用
json_last_error()检查解密后的JSON是否合法。一旦发现非法JSON,直接拒绝处理,不进入后续的任何数据库操作分支。
PHP参数化查询,类型标记不能省
用MySQLi做参数化查询,不是写完 prepare() 就万事大吉了。关键一步是 bind_param()——漏掉类型标记(比如 "si"),或者图省事用 query() 替代 execute(),那所谓的防护基本等于白做。很多框架虽然帮你封装了数据库操作,但你得确认它内部到底调了 prepare + execute,还是偷偷走了 PDO::query() 或者 mysql_query() 那种带变量插值的调用。
正确写法应该是这样:$stmt = $mysqli->prepare("UPDATE orders SET status = ? WHERE out_trade_no = ?"); $stmt->bind_param("ss", $status, $out_trade_no); $stmt->execute();。而像 $mysqli->query("UPDATE orders SET status = '$status' WHERE out_trade_no = '$out_trade_no'");这种,哪怕前面已经做了过滤,也属于高危的拼接操作,绝对不能出现。
幂等性和SQL注入是两回事,别混为一谈
很多人会把“幂等判断”当成救命稻草,认为查一下订单是否已经存在,再决定是否更新,就能挡住注入。这个认知是有问题的。幂等只负责解决重复通知的问题,它管不了参数内容是否被污染。哪怕你只执行一次 UPDATE,只要这条语句本身是拼接出来的,注入风险依然存在。
- 幂等逻辑本身,也必须用参数化查询来实现。比如
SELECT id FROM orders WHERE out_trade_no = ?。 - 不要指望
INSERT ... ON DUPLICATE KEY UPDATE能替代严格的白名单校验,因为ON DUPLICATE KEY UPDATE里涉及的值,如果来自未过滤的参数,同样会引来注入风险。 - 最保险的做法是:所有回调入口,第一件事就是白名单校验全部字段。校验不通过,直接
exit或返回一个标准错误码,绝不进入任何数据库操作的逻辑分支。
说实话,真正难防的,往往不是明面上那个明显的单引号闭合,而是一些看似合法、实则暗藏玄机的组合。比如 ORDER-123%00(URL编码的空字符)、abcu200bdef(零宽空格),或者用Unicode全角数字去替代ASCII数字。这类攻击手法比较隐蔽,因此规则必须定得足够严。建议用 ctype_print() 或正则 /^[[:ascii:]]+$/ 来限制只允许可打印ASCII字符,别凭“看起来像订单号”这种模糊的判断来做决策。
