最近在使用某个第三方提供的支付 sdk 开发的时候,发现一个问题,可能造成支付结果伪造。目前漏洞已经修复。
整个的流程大致是这样的,你的前端(app 或者网页)将姓名地址手机等订单信息发送到服务器,服务器创建订单,然后使用这个订单号创建一个该 sdk 的 charge 对象,返回给前端,sdk 会使用这个 charge 对象调用支付宝等进行支付。等待用户支付完成后,sdk的服务器会向你的一个异步通知 notify_url 推送消息,然后服务器根据消息里面的订单号和支付结果修改付款状态。其实这个和支付宝等官方的 sdk 流程没什么大的区别,只是帮助封装了一些加密解密的操作。
因为这异步通知的消息格式是确定的,字段的值是全部可预测的的(因为在 charge 对象里面都能看到),所以我们能不能伪造这个异步通知结果呢,这样的话,就能不付款买东西了。
但是问题来了,这个异步通知的 url 我们是不知道的。怎么才能获取到呢?其实是在一个 ajax 请求中泄露的。
登录管理控制台,链接是这个形式的,https://xxxx.com/app/detail?app_id=app_****,后面就是我的 app_id,而这个在前端和服务器交互数据中可以看到的。把我的 app_id 换成别人的,结果发现点击哪里都是显示应用不存在,但是使用浏览器开发者工具能看到一个 ajax 请求,https://xxxx.com/auto/app?app_id=***,返回结果确实是那个 app_id 的,里面就是包含 notify_url。
这样,将前面获取的订单号,app_id 等等直接构造一个 charge 对象,post 到那个 notify_url 就好了。服务器返回” success” 的话,就表明已经收到消息。
分析这个漏洞,sdk开发者和用户都有责任。
首先,sdk 开发者应该指导用户正确的去校验消息和确认支付状态。目前没有签名等校验方法,只能拿订单号再去查询一遍。而绝大多数的用户没有考虑的这么多,收到了通知就修改了订单状态。
翻了翻支付宝的文档,在 wap 版本中,它的异步通知 url 是每次自定义的,而且是不公开的。大致的流程是使用交易服务器使用订单号等换取一个 token,然后将 token 传递给前端。它的每次请求都有签名的,RSA 或者 MD5,RSA 的数据都是加密的,需要用自己的私钥解密后才能看到内容。MD5的那个是明文数据,MD5校验值是数据的一部分。这个时候,如果用户选择了 RSA 签名,基本上消息不能伪造了,因为在不泄露的情况下你不知道用户私钥(当然前面不泄露 notify_url 也不能伪造结果),就没办法去加密数据。MD5的话,只能自己去验证签名了,因为 MD5 的时候加入了用户私钥。
用户在使用的时候我觉得是最安全的方法是:
- 将 notify_url 复杂化
- 收到异步通知后再去查询一下付款状态
- 使用 https
- 绑定通知服务器 ip
- 注意不同平台之间差异
为什么说注意不同平台之间差异呢,因为后台给使用这个支付服务的 app 抓包的时候,发现上面提到的有些信息并不完全是未知的。app支付的流程并不一样,上面流程中使用订单号换取 token 的步骤没了,而是直接将数据传递给 app,这个时候 notify_url 也是公开的了。所以还是需要服务器校验。
在上面看了几个支付宝的文档,网页版的和 app 版本的应该不是一个团队写的,风格、加密方式等等都不一样。真心蛋疼。。。