sec-payment
Skill3 starsChanged 9 days agoMITView on GitHub
支付安全(在线收款、充值、余额、积分 / 额度、兑换码、优惠码、分佣、按量扣费):付款通知先验签名再去网关回查、 每笔钱只入账一次(原子更新 + 流水唯一号)、金额和价格只在服务器算、金额 / 币种 / 商户 / 正式环境逐项对、 通知丢了有兜底查单、盗卡充值和拒付的上限、退款作废分佣、免费额度 / 新人礼 / 试用防薅、兑换码怎么做才不被刷、 后台改余额的权限、运行时对账告警(余额只能因为有记录的充值变多)。 接任何支付网关之前、做充值 / 余额 / 额度 / 兑换码 / 分销之前、被刷了之后用。附回调验签 + 幂等入账的 PHP / Node 代码。 触发词:支付安全、付款回调、webhook 验签、重复入账、幂等、金额篡改、充值、余额、兑换码、卡密、优惠码、薅羊毛、 新人礼、盗刷、拒付、chargeback、分佣、对账、payment security, webhook signature, idempotency, double credit, fraud.
In this repository5 other files it gives its agents.
- sec-auditSkill
- sec-edgeSkill
- sec-secretsSkill
- sec-serverSkill
- sec-watchSkill
The file
Files
sec-payment:支付安全
一句话:客户端里没有秘密,浏览器传来的一个都不信,付款通知只信回查,每笔钱只入账一次,所有能“变出钱或额度”的地方都要对账。
起因:一个朋友的网站卖兑换码,被人刷走了大约 1000 美元的额度。我们照着把自己所有能变出钱或额度的地方查了一遍,下面是查出来的规矩和真实的坑。
代码示例见 references/webhook-examples.md(PHP 和 Node:验签 → 回查 → 原子入账 → 兜底查单 → 对账)。
0. AI 逆向时代:客户端里没有秘密
过去,拆一个 App、一段混淆过的前端、一个小程序,要熟练的逆向工程师花上几天;现在把代码丢给 AI,签名算法、接口地址、隐藏参数、你以为藏好的密钥,往往几分钟就摆在面前。混淆、加壳只能拖慢它几分钟。所以:
- 前端、App、小程序、桌面客户端里的一切,都当作攻击者已经看过。
- 客户端里不放任何能产生钱的密钥:支付网关密钥、签名密钥、管理接口 token、第三方 API Key。
- 客户端给请求签名、给参数加密,只能增加成本,不是安全边界。服务器照样每一项重新验证。
- 价格、数量、折扣、优惠资格、库存、额度,全部服务器重算。客户端传来的只是“我想买什么”。
- 设备指纹、行为验证码是信号,用来打分和限速,不能当唯一的门。
- 接口只返回这个页面需要的字段;报错不暴露内部细节——AI 读你的接口返回和报错,和读你的代码一样快,会拿它调整下一次尝试。
- 频率和金额的上限放在服务器:按用户、设备、IP、付款账户各算一份。
1. 付款通知(Webhook)
按这个顺序,少一步都不行:
- 验签名:按网关文档的算法(HMAC-SHA256 / RSA),用原始请求体算(不要先 JSON 解析再序列化,字段顺序、空格一变签名就对不上)。比较用常量时间函数(PHP
hash_equals,Nodecrypto.timingSafeEqual)。带时间戳的,超过 5 分钟的不收(防重放)。 - 回查:拿订单号调网关的查询接口,以查回来的状态和金额为准,不信通知里写的。签名密钥泄露、网关测试环境的通知误打到正式环境、有人重放旧通知,都靠这一步挡住。
- 逐项对:订单在你库里存在、状态是“待支付”、金额相等、币种相等、收款商户是你自己、是正式环境不是测试环境。任何一项对不上:不入账、推告警、人工看。
- 原子入账(第 2 节)。
- 入账成功(或者确认早就入过)才回 200;中途出错回 5xx,让网关重试。
回调地址本身:
- 只收 POST,请求体限制小(几十 KB),单独限流。
- CDN 的“一键防机器人”会把网关的回调当机器人拦掉,要给回调路径放行(
sec-edge§1)。 - 回调地址写在网关后台的,以后换域名时旧地址不能直接 301:网关 POST 跟着 301 跳转会丢请求体。旧域名上的回调路径要一直照收。
通知会丢:网关偶尔不推、推的时候你正好在部署、被 CDN 拦了。加一个定时任务,每 15–30 分钟把还没付的订单去网关查一遍,付了的补入账(走同一个入账函数),过期的改成已取消。
2. 每笔钱只入账一次
两个通知同时到、通知和兜底任务同时跑、用户狂点“我已付款”,都只能加一次钱:
UPDATE orders SET status = 'paid', paid_at = NOW() WHERE id = ? AND status = 'pending';
-- 影响行数 = 1 才继续加钱;= 0 说明已经入过账,直接返回成功- 加钱和改订单状态在同一个事务里。
- 流水表的
ref(比如order:123)建唯一索引:就算代码有 bug 走了两次,第二次插入也会失败。 - 余额只通过流水变:
余额 = 流水之和,可以随时对账(第 8 节)。 - 金额一律用最小单位的整数(分、或者没有小数的币种直接用元),不用浮点数。
- 要调用外部系统加额度(第三方平台、充值接口):先在订单上占位(
crediting_at = NOW()),调用成功再标完成。调用失败只推告警、人工处理,不要自动重试加额度——外部系统可能其实已经加上了。 - 测:拿一条有效通知,同时打两次(
seq 2 | xargs -P2 -I{} curl ...),余额只能加一次。
3. 金额和价格
- 价格、手续费、折扣、汇率、运费全在服务器算。前端算出来的金额只用来展示,提交上来的金额不用。
- 输入边界:负数、0、超长小数、超大数、科学计数法(
1e9)、全角数字。 - 币种的最小单位:有的网关按“分”收,有的币种本来就没有小数。接一个新网关,先拿一笔真单核对金额单位。
- 汇率从外部取的,要有上下限保护:汇率源出错时不能让东西变成半价。
- 开了折扣码之后,对账要用“实付金额”字段,不是原价字段。
- 转发给上游 / 第三方的参数,必须和你计费用的参数是同一份。我们踩过:数量参数只在算价格时截断到上限,转发出去的还是用户传的原始值——上游按 50 份收我们的钱,我们按 10 份收用户的钱。
4. 盗卡和拒付
用偷来的卡充值、把额度花掉、然后持卡人拒付:钱被网关扣回去,额度已经没了。
- 卡类付款给新客户设上限:单笔、24 小时累计都设(比如新客户单笔不超过平时客单价的几倍);有过一笔一周以前的成功付款的老客户再放宽。扫码、转账这类不能拒付的方式可以不限。
- 能开 3DS 就开。
- 接网关的退款 / 拒付通知:收到就冻结这个用户的余额和 API Key,作废他带来的佣金。
- 退款、拒付、取消订单,都要作废已经发出去的佣金和奖励;已经结算给推广者的,推告警追回。
5. 免费额度、试用、新人礼(最容易被薅)
损失 = 每份的金额 × 能注册的号数。所以:
- 一人一份要真的是一人:
- 邮箱规范化后再判断重复:小写、去掉
+后面的部分、Gmail 去掉点(a.b+x@gmail.com=ab@gmail.com);存哈希就够了。 - 一次性邮箱域名直接拒收(
sec-edge§7 的名单)。 - IPv6 按
/64算同一个网络(一个人手里有几十亿个 IPv6 地址,按完整地址判断等于没判断)。
- 邮箱规范化后再判断重复:小写、去掉
- 先占位再检查,不要先查后写:先插一条“领取记录”(唯一索引兜底),再检查有没有超;超了把刚插的退回。先查后写,并发 10 个请求能领 10 份。
- 每天总量上限:最坏情况下损失也有个顶。
- 试用只给便宜的选项;输入长度、次数都限制。
- 注册、领取开人机验证(
sec-edge§8)。
6. 兑换码、优惠码、卡密
能不做就不做。必须做:
- 码要足够长且真随机(至少 128 位随机数,
random_bytes(16)转成 base32),不能是递增编号、不能能猜。 - 数据库里存哈希,不存原码:库被拖走也拿不到能用的码。
- 一码一用:兑换走原子更新
UPDATE codes SET used_by = ?, used_at = NOW() WHERE code_hash = ? AND used_by IS NULL,看影响行数。 - 兑换接口按用户、按 IP 限速,连续输错锁定。
- 生成和作废码的人、时间、数量都留记录;后台批量生成要最高权限。
7. 后台和员工
- 改余额、改价格、改付款设置、改管理员:单独权限 + 两步验证 + 操作记录 + 每次推告警。
- 能重置客户密码 / 关客户两步验证 / 发登录链接的员工,就能接管客户的余额。这些操作只给最高管理员,或者每次要输动态码并邮件通知客户。
- 一次性登录链接、跨站登录链接要绑定发起的浏览器:否则攻击者能把受害者登录到攻击者的号上,受害者再付款,钱就付进了攻击者的号。
- 自己推荐自己:推广者第一次结算前人工看一眼,比对收款人和付款人的信息。
8. 对账告警(运行时)
代码审得再细也会漏,所以要有一个每 15 分钟跑一次的对账任务,推到告警群:
- 余额 / 额度只能因为有记录的充值变多:每个用户“剩余 + 已用”比上次多出来的部分,找不到对应的充值单或赠送记录就报。兑换码漏洞、有人在后台手动加、管理员 Token 泄露,都会在一个小时内被发现。
余额 ≠ 流水之和的用户;出现负余额的用户。- 加了额度但订单状态不是“已到账”的。
- 一小时内某个用户充值金额 / 笔数异常;新人礼一小时送出太多;同一个网络一天注册多个号;试用账户花得太快。
- 同一件事 6 小时内不重复报;阈值放配置里,不写死在代码里。
按用量扣费的接口(API、短信、云资源)还要注意:
- 先预扣再执行:按“最多可能花多少”预扣,余额不够就把这次的上限压小或者直接拒绝,执行完多退少补。
- 结算补扣时余额不够只扣到 0、差额记日志,任何并发下都不能扣成负数。
- 子 Key / 子账号的额度上限,要在所有扣费入口都生效(我们踩过:新加的接口忘了检查子 Key 的额度,限额 1 美元的子 Key 能花掉整个账户)。
9. 卡号等支付数据
- 能让网关托管(返回一个 token)就别自己存卡号。
- 必须存的:见
sec-secrets第 3 节(双层加密、绑定记录、看明文要二次验证 + 留痕 + 告警);存完整卡号要看 PCI DSS 的要求。 - 卡号、CVV 永远不进日志、不进邮件、不进群聊。
- 客户后台、后台页面
Cache-Control: no-store, private。
改完的检查清单
- 伪造一条通知(签名不对)→ 401,不入账
- 用一条真通知改金额重放 → 回查发现对不上,不入账、推告警
- 同一条真通知并发打两次 → 只加一次
- 停掉通知,兜底任务能补上入账
- 前端改价格 / 改数量提交 → 服务器按自己算的收
- 同一个 Gmail 加点、加
+、换 IPv6 地址,新人礼只能领一份;并发领也只有一份 - 对账任务手动往库里加一笔没有来源的额度 → 15 分钟内收到告警
- 退款 / 拒付后佣金作废
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

