短信接口安全测试实战:从漏洞发现到防刷加固 我接到的是一个常规授权测试任务目标是某安全厂商的官网。前端的注册、登录、密码找回、绑定手机号这些都是验证码短信的高频入口。测试还没深入后台一个不起眼的短信发送接口就暴露了明显问题它既没有来源校验也没有有效的频控手机号参数完全由前端决定。这类接口问题在安全测试里属于典型的“低技术含量、高业务影响”但造成的损失非常直观——短信通道按条计费脚本一旦跑起来可以在很短的时间内把预充值费用刷掉数万元。这篇文章就以这次测试为背景聊聊短信接口安全测试该怎么测、这类隐患为什么反复出现、以及从发送源头到业务侧应该怎么堵。适合三类人看正在做渗透测试的安全工程师、负责账号体系或消息系统的后端开发还有想自己检查官网短信成本风险的运维和业务负责人。1. 短信接口为什么值得专门做一轮安全测试大多数团队对短信接口的态度是“能用就行”它能发出去、验证码能校验通过就算完成需求。可在安全视角下短信接口是一个直接与资金挂钩的对外暴露面它的失控成本远超普通接口漏洞。1.1 一条短信背后牵着的真金白银先算一笔账。假设通道商的结算价是0.045元/条这个价格在市场上很常见。一个没有任何频控的短信接口自动化脚本用并发线程持续调用实际打出来的速率可以非常夸张。即便按单线程每200毫秒发一条来算一小时也能发出1.8万条成本差不多810元。如果无人发现持续一个晚上就是8000多元。配合代理池和号码池把速度提上去几小时内烧掉数万元完全不是夸张说法。更麻烦的是通道封停。很多通道商对发送量、发送频率、投诉率都有硬性指标短时间爆发会被直接关停。通道一封全平台的验证码业务跟着停摆所有用户都没法正常登录注册。这时的损失就不只是短信费了还有整个平台的可用性。1.2 短信接口在业务里的暴露面比想象中大登录、注册、找回密码、更换手机号、修改支付密码、二次验证……只要业务有账号体系短信接口就会出现在多个页面。每个入口都是同一个后端能力的复用只要其中一处没有控制好等于所有入口都开放。测试时尤其要注意“找回密码”这类场景。登录注册页好歹有图形验证码找回密码往往为了“用户体验”把验证前置条件做得极简有的连图形验证码都没有只有手机号和发送按钮。这种接口一旦被脚本盯上发出去的每一条短信都是成本而且面向的用户群体里有大量真实手机号还会引发骚扰投诉。1.3 这次测试的边界与前提这次测试是在书面授权范围内进行的目标是“某安全厂商官网”本文统一称为被测站点A。需要强调一句对任何站点做这类测试都必须提前确认授权范围尤其是短信接口会真实消耗费用、真实触达用户测试量必须严格控制在最小范围内。这篇文章里所有验证动作都是在授权环境下少量请求、用自查号码完成的不会提供批量发送的利用脚本。目的是让开发和安全同学理解漏洞原理和修复方案而不是教人刷接口。2. 从信息收集到风险验证一次完整的短信接口测试实录短信接口的测试链路并不长但每一步都可以拆出细节。下面按我实际操作时的顺序拆开讲。2.1 第一步在页面上找到所有会触发短信的下发点打开被测站点A的域名注册一个新账号用浏览器开发者工具的网络面板盯着点击“获取验证码”按钮观察发出的Ajax请求。常见路径有/api/sms/send、/captcha/send、/auth/sendCode这一类的具体的路径名不重要关键是要抓到原始的请求包。请求包里面的参数常见的几类是这样的参数名含义测试关注点mobile / phone / tel手机号是否由前端直接指定而不经服务端重新获取type / scene / businessType短信场景类型是否可以被改成其他场景拿到不同的模板token / ticket前端人机验证通过的凭证是否可以缺失、是否可以重放source / clientType来源标记是否被服务端信任能否随意伪造抓包之后第一步不是急着改参数而是先看看有没有明显的异常。用浏览器直接访问这个发送接口的URL如果返回了“请通过页面访问”之类的提示说明至少有一道校验如果直接返回参数错误或成功的JSON那入口就是裸奔的。2.2 第二步把请求拆开逐项确认参数是否可信拿到原始请求后我习惯一个个参数试原则是“一次只改一个变量观察服务端反应的变化”。第一项是手机号。把mobile参数替换成一个虚构的测试号码如果服务端仍然返回“发送成功”说明这个参数完全可信。这里的关键不是替换本身而是确认服务端在生成验证码时是否真的拿这个参数作为接收方。有的系统会用会话里的手机号前端传的参数只是展示用那问题就不在这。第二项是场景参数。短信接口经常用type来区分“注册验证码”“登录验证码”“营销通知”服务端根据type加载不同的短信模板。把type从注册改成其他值如果返回成功并且收到的短信内容风格发生了明显变化说明模板被成功切换了。这个风险再往前走一步就是有人可以通过控制模板内容把一个验证码接口变成自由短信网关。第三项是来源校验。很多接口不带登录态就能调用因为验证码发送本身就是匿名场景。这时就看服务端有没有校验来源页面。现实中相当多的实现只检查了Referer请求头或者干脆不检查。而Referer这种请求头是客户端随意可控的用命令行工具指定一个页面地址就能轻松绕过。2.3 第三步低噪声验证限频与来源校验是否生效限频是否生效是这次测试里最核心的判断。做法很简单同一个测试号码大约间隔一分钟请求一次连续请求三到四次观察每次的响应。如果每次都返回成功说明按号码维度的短时频控不存在或阈值很高。再换一种测法用不同的号码去请求但都走同一个出口IP观察单位时间内IP维度的限频是否存在。如果接口从同一样IP连续发出去几十条都成功那基本可以判断IP维度的频控也缺失。这里我强烈建议验证动作做到这个程度就足够了。安全测试的目的是判定风险是否存在而不是证明它到底能达到多大的破坏力。我曾见过有测试人员为了写报告把一个短信接口打了上千条最后通道被封业务方直接炸锅。正确的做法是少量请求、观察状态码变化、用响应差异推导逻辑。比如第一次请求成功、第二次也成功、第三次仍然成功就已经可以判定“无频控”不需要再发更多条去证实。2.4 测试过程中的自我保护量级控制与数据脱敏短信接口测试最容易踩的坑就是“测着测着把自己变成攻击者”。除了控制请求量还要注意使用的号码。最稳妥的方式是准备一个专用测试号码而不是拿真实用户手机号测。如果被测环境支持测试白名单那更好提前在通道商后台把号码加进去既能验证功能又不会产生费用和骚扰。其次观察接口响应时注意检查是否存在“验证码明文放在响应里”的低级问题。有一次测试我点开发送验证码接口的响应JSON里面直接把四位数字验证码放到data字段里了。这种问题不是漏洞利用的难点问题但影响面极广属于必然要写进报告的高危项。3. 隐患的根因为什么这类问题总是反复出现短信接口漏洞在代码层面并不复杂但它在真实项目里的出现频率高得吓人。总结起来根子还是出在开发对这块功能的定位上。3.1 发送与校验被当成“简单功能”而不是“资金出口”很多后端的Controller代码里发送验证码就是一个十几行的接口接收手机号、生成随机数字、调用短信SDK、落库。开发者没有意识到这十几行代码背后是一条按条计费、面向全网开放的通道。同样一段代码如果是发送电子邮件的接口被脚本刷一下最多就是邮件服务商的风控提示损失几乎为零。但换成短信每一条都在产生成本单位时间内对外可被调用的频率直接对应资金消耗速度。这种定位偏差导致了一系列连锁缺失不做频控、不做总量控制、不设熔断、不接入告警。常规功能测试又只会验证“成功发送”和“验证码能登录”不会有人去关心“这个接口能不能被陌生设备连续调用一小时”。3.2 限频设计只覆盖单维度绕过思路一抓一大把即便有的团队做了频控也常常只做一个维度。最常见的是只限制同一手机号一分钟只能发一次。这个限制在真实攻击场景面前基本等于没有攻击者手里有号码池换一个号码继续调用一分钟一次的限制完全不影响批量发送。而且这类限频通常是用本地内存计数器实现的只对单实例生效应用一扩容、负载一均衡频控各自独立效果又在打折。更隐蔽的问题是频控键设计不恰当。有些系统把频控key设计成拼接了手机号和场景例如register:138xxxx和login:138xxxx是两条独立的计数。如果一个业务有九个场景攻击者按顺序轮询九个场景每个场景的限额都是充足的等于是把限制拆成了九份。3.3 参数表面上是“场景标识”实际上可以被任意改写开发给type参数加注释时通常会写“短信场景类型1注册2登录”。可在服务端如果只拿这个type去查模板而不校验它跟当前页面流程是否匹配那这个参数就成了可被任意利用的开关。攻击者完全可以把“注册验证码”接口改成发营销短信、发通知短信内容模板都由参数决定。同理来源校验如果只检查Referer请求头也是非常薄弱的。请求头来自客户端用curl、浏览器插件、脚本都能随意指定。真正可靠的来源校验是服务端生成的一次性票据页面先调用人机验证接口拿到服务端签发的短期token发送短信时再校验这个token。token要在使用后立即失效并且与手机号、场景绑定防止重放和窜改。4. 从发送源头到业务侧短信接口加固的完整方案发现问题之后修复方案不能只停留在“加个if限制一下”。真正有效的加固需要覆盖参数校验、频控、人机校验、熔断、监控五个层面。4.1 服务端强制参数校验与模板化下发短信内容永远不应该由前端传进来。前端传的只能是场景类型后端根据场景类型从模板库加载内容模板库里的参数也要做白名单处理。手机号参数必须做严格的格式和号段校验这一点上代码里很常见但经常被漏掉的是国家码有些接口允许传国家码参数如果不做白名单攻击者可以把一条短信发到任意境外号码上成本更高。下面这段是发送前检查的最小化的逻辑核心点在于先校验参数再检查频控最后才真正调通道public SendResult sendVerifyCode(String mobile, String type, String clientIp, String deviceId) { // 1. 手机号格式校验符合中国大陆11位手机号规范 if (!Validator.isValidMobile(mobile)) { return SendResult.invalid(手机号不合法); } // 2. 模板必须存在于模板库且处于启用状态 SmsTemplate tpl smsTemplateService.getByType(type); if (tpl null || !tpl.isEnabled()) { return SendResult.invalid(模板不可用); } // 3. 前置票据校验这里校验从人机验证服务拿到的 ticket if (!ticketService.verify(ticket, mobile, type)) { return SendResult.invalid(人机验证未通过); } // 4. 频控同一号码、同一 IP、同一设备三个维度都查 if (rateLimiter.isLimited(sms:m: mobile, 1, Duration.ofMinutes(1))) { return SendResult.busy(同一号码请求过于频繁); } if (rateLimiter.isLimited(sms:ip: clientIp, 20, Duration.ofMinutes(10))) { return SendResult.busy(当前网络请求过多); } if (rateLimiter.isLimited(sms:d: deviceId, 5, Duration.ofHours(1))) { return SendResult.busy(设备使用频率超限); } // 5. 全站当日总量检查 if (dailyCounter.isReachedMax(sms:total, 100000)) { return SendResult.busy(今日短信总量已达上限); } return smsChannel.send(mobile, tpl, buildParams(mobile, type)); }这段代码里比较容易被忽略的是票据校验放在频控前面。如果先查频控再有票据校验攻击者可以用大量无效票据做探测把频控计数打满造成合法用户发送失败。顺序这种事在真实故障里经常是坑。4.2 多维频控用户维度、设备维度、IP维度、全站总量频控维度需要按风险等级区分不是说所有维度都要紧。我的建议是四层都设但阈值从宽到严排列限制维度限制粒度建议阈值说明手机号维度1条/分钟严格正常业务下同一号码一分钟内不应收到第二条验证码设备维度5条/小时适中设备ID可以从请求头或前端埋点获取篡改成本略高IP维度20条/10分钟适中考虑出口NAT场景阈值要大于办公网和家庭宽带的正常量全站总量日上限动态按历史日峰值乘以1.2到1.5倍超限自动停线次日恢复频控实现层面不要用本地内存计数器多实例部署时会失效。推荐用Redis这类集中式存储配合原子自增和过期时间。注意频控的“单位时间窗口”最好用滑动窗口固定窗口在临界点会被打满双倍请求。4.3 发送源头的前置人机校验短信接口前面扣一道图形验证码或滑块是目前性价比很高的防线。人机验证的目的不是让攻击者过不去而是显著提高自动化调用的成本。关键在于人机校验必须在服务端完成并且要返回一次性票据。前端的图形验证码再怎么炫只要后台不校验结果就是摆设。票据要有有效期一般在60到90秒内有效使用后立即失效还要跟手机号绑定。如果票据没绑手机号攻击者可以通过先过一遍人机验证拿票据再把这个票据用到任意手机号上。4.4 成本熔断与异常监控限频和验证码拦不住所有情况所以最后一道防线是成本熔断。通道商后台一般都有日发送量限额设置这个必须开。比如平台正常一天发10万条就把通道商的限额设到15万条达到即停发。这是外部兜底防止应用层逻辑被绕过时短信费失去控制。平台内部还要有总量监控用Redis或数据库统计当天的发送量一旦超过阈值自动触发熔断开关。同时接入告警把发送量异常增长告警发送到值班群比如环比前一天同时段增长超过50%就告警。这是我见过很多公司忽略的一环应用层的频控做得再严一旦上线配置错了或者被绕过了没有任何熔断机制兜底损失还是会发生。正规的安全测试报告也会建议把“成本熔断”列为必须整改项。4.5 修复后的回归验证加固完成后不能只让研发说一句“改好了”就结束。测试人员要复测同样的路径用同一个测试号码连续请求确认第二次开始返回频率限制换不同号码同IP请求确认IP维度限制生效绕过人机验证直接调用接口确认返回未通过修改type参数确认模板仍然不对外暴露。回归的重点是确认“限制策略没有误伤正常业务”。频控阈值设得太低会导致用户在WiFi环境下登录大量手机号时被限制这种问题在测试环境不容易暴露。建议复测的时候模拟一下“一个手机号两台设备同时登录”这类正常场景确保风控规则不会把真实用户挡在门外。5. 测试之外报告输出与后续跟进的经验漏洞发现和修复之外还有一批容易被忽略的工作恰恰是决定一次安全测试价值能不能落地的关键。5.1 报告要写清楚“为什么会损失数万”给业务方和研发看的安全测试报告不能只写“短信接口存在未授权访问漏洞等级高危”这句话太干。更有说服力的写法是先说业务影响再给技术细节。我在报告里一般会这样组织漏洞位置被测站点A的短信发送接口路径/api/sms/send风险描述接口未校验来源、未限制频率、手机号参数可任意指定且响应中可直接看到发送结果影响范围全平台所有依赖验证码登录、注册、找回密码的业务入口风险等级严重可直接导致资金损失和通道封禁复现条件任意可联网设备无需登录无需图形验证码加固建议服务端模板化下发、多维频控、人机校验、全站总量熔断、监控告警复测结果修复后同一场景验证限制生效风险关闭把“按0.045元/条计费”的成本模型放到报告里业务方才会意识到这不是“一个技术小Bug”而是与预算直接挂钩的风险项。5.2 与研发沟通的节奏先讲业务后果再讲技术细节短信接口的问题研发第一反应往往是“这只是一个内部接口外网不知道路径”。这种认知需要先纠正。要让研发明白路径可以被枚举、前端页面源码会泄露、历史版本的JS文件可能还保留着旧接口地址。沟通时先讲“这个接口可以被脚本无限刷、每个小时可能烧掉几千块钱、通道会被封停”再讲具体修复方案。实际项目里跟研发对齐“频控要在服务端做而不是只在前端做”“票据要一次性使用”“模板参数要做白名单”这三个点修复质量基本就有保证了。我遇到的大多数修复不到位的情况都是研发只理解了其中一两点导致上线后还是能绕过。5.3 特别提醒别在测试阶段把自己变成攻击者做短信接口测试时身份很微妙测试人员既有责任验证风险又很容易在验证过程中越界。我的原则是三条请求量控制在个位数到两位数之间只使用自己可控的测试号码不保存任何真实用户信息。报告里也只提风险和修复方案不放批量利用代码。授权范围之外的动作哪怕再顺手也不应该做。这些规矩不只是为了保护被测系统也是保护测试人员自己。安全测试圈子里因为测试行为过界引发争议的例子不在少数原因基本都是忽略了“最小验证”原则。最后分享一个我自己的小习惯。每次测完短信接口我都会在报告末尾加一条额外提醒——让负责通道的同事在通道商后台把测试号码加进白名单同时确认日发送量限额真的已经开启。这两个动作都落实了短信接口这轮测试才算真正收尾。看完这篇文章你也可以自己检查一下平台的短信发送接口它有没有做服务端频控、有没有模板白名单、有没有成本熔断。别把它当成一个普通的业务辅助功能它的失控成本远比想象中直接。