短信验证码被刷怎么防:六类攻击、四层防御与接口层的三个坑
短信验证码被刷到一夜五位数账单的原因与防御:六类攻击方式(含最易漏的单号码轰炸)、按性价比排序的四层防御(人机校验/四维频控/风险识别/监控熔断)、验证码设计六个细节,以及接口层三个真实坑——内容必须带签名否则1009、短信通道签名不含tm、业务层重发会与通道自动重发叠加。
短信验证码被刷,是少数几个"一夜之间账单变五位数"的技术事故。更麻烦的是,它通常不是漏洞被攻破,而是你的接口被当成免费短信通道用了。
这篇讲清六类攻击怎么打进来、四层防御怎么按成本排队,以及接短信接口时三个真实存在的坑。
一、先看攻击是怎么发生的
一个典型场景:注册页发出验证码,接口带 phone 参数,没有任何前置校验。
攻击者的做法比你想的简单:
注意一个反直觉的点:攻击者的收益往往不是撞库成功,而是把你变成骚扰短信的发射源——他要的是让这批号码收到短信(比如用来做诈骗前置、给竞争对手制造投诉、或者纯粹消耗你的成本)。
所以防御的核心目标不是"防破解",是防滥用。
二、六类攻击方式
| # | 攻击方式 | 特征 | 主要防线 |
|---|
| 1 | 脚本撞库 | 单一 IP 高频请求,号码连续/规律 | 频率控制 + IP 维度 |
|---|---|---|---|
| 2 | 短信轰炸 | 针对同一个号码大量请求,让受害者被骚扰 | 手机号维度频控(最容易漏) |
| 3 | 接码平台(卡商) | 使用虚拟号段/一人多号完成注册,绕过手机号唯一性 | 号段识别 + 设备指纹 + 业务风控 |
| 4 | 接口逆向直连 | 抓到你的 App 包,直接调你的发送接口,绕过前端全部校验 | 服务端签名 + 渠道校验 |
| 5 | 内部泄露 | 商户密钥流出,攻击者以你的名义发短信 | 密钥分环境 + 轮换 + 调用明细告警 |
| 6 | 人机混合(打码平台) | 人工/打码服务过图形验证码,再发起请求 | 行为验证 + 风险评分,不指望单一验证码 |
第 2 类最容易被忽略:多数团队都会做 IP 频控,但没有做"单号码频控"。结果是攻击者用 3 万个 IP、每个 IP 只请求一次,同时让某个受害者一分钟内收到 200 条短信——技术上"通过了限流",业务上已经出事(投诉、封号、监管问询)。
三、四层防御:按性价比排序,别一次全上
第 1 层:前置人机校验(性价比最划算)
发码前必须有一步不可自动化的验证:图形验证码、滑块、行为轨迹、点选。
第 2 层:多维频率控制(核心)
单一维度必然被绕过,最少要四维同时生效:
| 维度 | 建议阈值(示例) | 作用 |
|---|
| 手机号 | 60 秒 1 次,24 小时 ≤ 10 次 | 挡短信轰炸 |
|---|---|---|
| 设备指纹 | 24 小时 ≤ 20 次 | 挡一人多号、接码平台 |
| IP | 小时 ≤ 30 次,突发峰值限速 | 挡脚本撞库 |
| 账号/会话 | 登录失败 5 次锁 15 分钟 | 挡已注册账号被盗刷 |
关键设计:四维是与关系不是或关系——任一维超限就拒绝。只做 IP 维度是最常见的半成品。
第 3 层:风险识别(决定要不要发)
频控解决"频",风险识别解决"这个人可不可信":
这一层的产出应该是一个风险分,而不是二值判断——分数高的走"额外验证",而不是直接拒绝(直接拒绝会误伤真实用户)。
第 4 层:监控与熔断
必须有,但排在最后:
四、验证码本身的设计细节
这些细节不涉及防御架构,但都是真实事故来源:
□ 6 位数字,5 分钟有效(再长就是给别人时间窗)
□ 一次有效:验证成功后立即失效,不能重复使用 □ 错误次数上限:同一验证码错 3-5 次即作废,防暴力枚举 □ 与手机号绑定校验:A 号码的验证码不能用 B 号码提交 □ 服务端存储 + 单次消费(用 SETNX/条件更新,防并发双花) □ 日志里不明文落库验证码(否则日志系统成了验证码仓库)
最后一条经常被忽略:验证码写进日志(log.info(f"send {code}"))等于把 5 分钟内的账户控制权暴露给所有能看日志的人。
五、接短信接口时的三个坑
这三个都写在对接文档里,但都容易在初版实现时踩到。
坑 1:短信内容必须带【签名】,否则直接被拒
内容字段里没带【企业签名】的请求不会发出去,返回错误码(我们这个通道是 1009)。联调时"提示发送成功但用户收不到"的情况,先确认内容是 【签名】您的验证码是 123456,5 分钟内有效。 这个结构。
坑 2:签名算式与同类接口不一致——不含时间戳
sms 通道:sign = md5(mall_id + phone + code + appkey) ★ 没有 tm
核验类通道:sign = md5(mall_id + realname + idcard + tm + appkey)
团队通常会把签名封装成一个通用函数复用。短信通道没有 tm 参数,如果通用函数固定拼接时间戳,签名必然对不上,而且报错信息往往只给"签名错误",很容易被误判成密钥问题查半天。
建议:签名函数按接口分别实现,或明确传入参与签名的字段列表,不要写死。
坑 3:自动重发会与通道自带重发叠加,造成重复扣费
部分通道(含我们的)对 5 分钟内未送达的短信会自动重发一次,仍失败自动退费。
这意味着:如果你在业务层再写一个"30 秒没收到回执就重发"的逻辑,就可能出现同一条短信被发两次——用户收到两条验证码、你被扣两条费用、回执维度还难以对账。
正确做法:业务层不自动重发,走"用户手动点击重发 + 手机号频控";如果一定要做自动化重发,先确认通道的重发策略并与回执状态对齐。
六、被刷了之后怎么止血
顺序很重要,别一上来就关接口:
1. 开熔断:全站验证码切到"图形验证 + 人工复核"(业务降级但不停服)
定位维度:拉最近 1 小时发送明细,按 IP / 号段 / 设备分别聚合,
找出集中维度(通常 1-2 个维度占 90% 以上)
拉黑:对该维度加黑名单 + 收紧阈值
通知:涉及向真实用户乱发的,按平台规则报备(这一步别省)
复盘:把这次的维度写进告警规则,防止换个维度再来一次
第 4 步是很多人不做、但后果最重的:通道被投诉到一定程度会被运营商限流甚至封停,影响的是后续所有正常业务,成本远高于那一晚的账单。
七、上线检查单
□ 发码前有人机校验,且服务端校验 + 一次性 token
□ 频控四维齐全:手机号 / 设备 / IP / 账号,且为"与"关系 □ 单号码频控已配置(防短信轰炸) □ 验证码:6 位、5 分钟、一次性、错误 3-5 次作废 □ 验证码不明文进日志,不落库明文 □ 签名按接口实现(短信通道不含 tm),不套用统一函数 □ 业务层不叠加自动重发,重发走用户手动 + 频控 □ 发送量突增告警 + 一键熔断开关 □ 密钥分环境隔离,日志脱敏 □ 有报备流程预案(被投诉时怎么处理)
相关阅读
短信通道的计费按条约定、签名与模板需报备,具体与商务确认;新用户有免费试用额度可用于联调。