网站短信接口被恶意调用,本质上不是攻击者技术有多高明,而是接口设计时把“能被调用”当成了“应该被调用”。短信接口的每一次请求都直接产生费用,一条验证码几分钱,攻击者用脚本一分钟刷几百条,一天下来账单可能多出几千块。更严重的是,恶意调用往往伴随批量注册、撞库、骚扰用户等后续行为,所以防护必须从接口逻辑、参数校验、频率限制、行为分析四个层面同时下手,而不是只加个图形验证码就完事。
先看最常见的漏洞形态。很多开发者在写短信发送接口时,直接把手机号作为唯一参数传给后端,前端没有任何校验,后端也只判断手机号格式是否正确。攻击者抓包后,用脚本遍历手机号段,就能批量触发短信。这种接口等于把发送按钮直接交给了攻击者。还有一种情况是验证码校验和发送逻辑分离不彻底,攻击者可以绕过前端倒计时,直接重复请求发送接口。倒计时只是前端体验优化,不能作为安全手段。
防护的第一步是确认“请求者是人”。图形验证码是最基础的方案,但不要用简单的四位数字或静态图片。滑块验证、点选验证、行为式验证的破解成本远高于普通图形验证码。关键点在于,验证码校验必须放在发送短信之前,并且校验结果要绑定当前会话或设备指纹,不能校验通过后返回一个固定token让攻击者复用。验证码服务端校验通过后,生成一次性凭证,凭证有效期控制在两到三分钟,且与手机号绑定。
// 发送短信接口的核心校验逻辑示例
function sendSms(req) {
const { phone, captchaToken, sessionId } = req.body;
// 1. 校验图形验证码凭证是否有效且未使用
const captchaValid = verifyCaptchaToken(captchaToken, sessionId);
if (!captchaValid) {
return { code: 403, msg: "验证码校验失败" };
}
// 2. 校验该凭证是否已绑定过其他手机号
const boundPhone = getCaptchaBoundPhone(captchaToken);
if (boundPhone && boundPhone !== phone) {
return { code: 403, msg: "凭证与手机号不匹配" };
}
// 3. 标记凭证为已使用,防止重复利用
markCaptchaTokenUsed(captchaToken, phone);
// 4. 后续频率限制和风控判断
// ...
}
第二步是频率限制,但频率限制不能只按手机号做。攻击者可以用海量不同手机号绕过单号限制,所以必须叠加IP限制、设备指纹限制、会话限制。单IP每分钟最多请求三次,单设备每天最多触发十条,单手机号每小时最多三条、每天最多五条。这里的数值要根据业务场景调整,但原则是“宁可误伤少量正常用户,也不能给攻击者留批量空间”。频率限制的计数要放在服务端,用Redis等缓存实现原子递增,避免并发请求绕过。
第三步是业务逻辑上的细节处理。短信验证码本身要设置有效期,通常五分钟,过期作废。验证码校验失败次数要限制,同一个验证码连续错误五次立即失效,防止暴力破解。发送接口要校验手机号是否在业务允许的号段内,比如只允许中国大陆手机号,就严格匹配正则,不匹配的直接拒绝,减少无效请求。另外,不要在接口返回中泄露“手机号未注册”或“手机号已注册”这类信息,统一返回“发送成功或失败请稍后再试”,避免攻击者利用短信接口探测用户注册状态。
第四步是引入行为分析和风险评分。单纯靠规则限制,攻击者可以通过更换IP、模拟设备来绕过。需要在发送请求时采集更多维度的数据:请求时间、页面停留时长、鼠标轨迹、设备型号、浏览器指纹、历史行为记录等。如果一个设备在短时间内更换多个手机号请求发送,或者一个IP在凌晨时段高频请求,这些行为模式与正常用户明显不同。可以将这些特征输入风控系统,对高风险请求直接拒绝或要求二次验证。中小站点可以接入第三方风控服务,不必自己从零搭建。
还有一个容易被忽略的点是短信服务商侧的防护。很多企业只在自己服务器上做限制,但短信服务商提供的API Key和Secret如果泄露,攻击者可以直接绕过你的服务器调用服务商接口。所以短信服务商的密钥要严格保管,不要写在前端代码或公开仓库里。在服务商后台也要设置发送上限和告警阈值,比如单日总发送量超过预算的80%就触发通知。服务商通常也支持IP白名单,只允许你的服务器IP调用,这样即使密钥泄露,攻击者也无法从其他IP发起请求。
对于已经遭受恶意调用的站点,首先要立即下线短信发送接口或切换为人工审核模式,然后分析日志找出攻击特征。日志中要记录每次请求的IP、手机号、设备指纹、时间戳、验证码校验结果、发送结果。通过日志聚合,可以快速定位攻击者的IP段、使用的手机号段、请求频率。封禁IP是临时手段,攻击者可能更换IP,但结合设备指纹和行为分析,可以更精准地拦截。同时检查是否存在其他漏洞,比如接口未鉴权、验证码可绕过、前端逻辑泄露等。
防护方案要落地成代码和配置,而不是停留在文档里。以下是一个综合防护的接口逻辑示例,展示了多层校验的顺序和关键判断点:
// 综合防护的短信发送接口伪代码
async function sendSmsWithProtection(req, res) {
const ip = getClientIp(req);
const deviceId = req.body.deviceId || generateDeviceId(req);
const phone = req.body.phone;
const captchaToken = req.body.captchaToken;
// 第一层:IP黑名单和频率检查
if (isIpBlocked(ip)) {
return res.status(403).json({ code: 403, msg: "请求过于频繁" });
}
const ipCount = await redis.incr(`sms:ip:${ip}:minute`);
if (ipCount === 1) await redis.expire(`sms:ip:${ip}:minute`, 60);
if (ipCount > 3) {
await blockIp(ip, 3600);
return res.status(403).json({ code: 403, msg: "请求过于频繁" });
}
// 第二层:设备指纹频率检查
const deviceCount = await redis.incr(`sms:device:${deviceId}:day`);
if (deviceCount === 1) await redis.expire(`sms:device:${deviceId}:day`, 86400);
if (deviceCount > 10) {
return res.status(403).json({ code: 403, msg: "该设备今日发送次数已达上限" });
}
// 第三层:图形验证码校验
const captchaValid = await verifyCaptcha(captchaToken, ip);
if (!captchaValid) {
return res.status(403).json({ code: 403, msg: "验证码错误" });
}
// 第四层:手机号格式和号段校验
if (!isValidPhone(phone)) {
return res.status(400).json({ code: 400, msg: "手机号格式不正确" });
}
// 第五层:手机号频率检查
const phoneHourCount = await redis.incr(`sms:phone:${phone}:hour`);
if (phoneHourCount === 1) await redis.expire(`sms:phone:${phone}:hour`, 3600);
if (phoneHourCount > 3) {
return res.status(403).json({ code: 403, msg: "该手机号请求过于频繁" });
}
// 第六层:风控评分
const riskScore = await riskEvaluate({ ip, deviceId, phone, timestamp: Date.now() });
if (riskScore > 80) {
await blockIp(ip, 7200);
return res.status(403).json({ code: 403, msg: "请求被拒绝" });
}
// 第七层:生成验证码并调用短信服务商
const code = generateSmsCode();
await saveSmsCode(phone, code, 300);
const sendResult = await smsProvider.send(phone, code);
if (!sendResult.success) {
return res.status(500).json({ code: 500, msg: "短信发送失败" });
}
// 记录日志用于后续分析
await logSmsRequest({ ip, deviceId, phone, riskScore, timestamp: Date.now(), success: true });
return res.json({ code: 200, msg: "发送成功" });
}
这个伪代码展示了多层防护的串联逻辑,每一层都在消耗攻击者的成本。攻击者要绕过图形验证码,需要接入打码平台;要绕过IP限制,需要大量代理IP;要绕过设备指纹限制,需要模拟不同设备环境;要绕过风控评分,需要模仿正常用户行为。这些成本叠加起来,大部分攻击者会选择放弃,转向防护更弱的站点。
另外,短信接口的防护还要考虑用户体验的平衡。频率限制太严格会误伤正常用户,比如用户在信号不好的地方多次点击发送按钮。可以在前端做防重复提交,按钮点击后置灰并倒计时,但这只是体验优化,后端限制才是底线。对于误伤的情况,可以提供申诉或人工客服通道,让正常用户能够解除限制。同时,把限制策略做成可配置的,根据业务高峰低谷动态调整阈值。
最后要强调的是,防护不是一次性的工作。攻击者会不断研究新的绕过方式,比如利用IPv6的大量地址、使用云函数动态IP、模拟真实浏览器环境等。所以需要定期审查短信接口的日志,分析异常模式,更新风控规则。短信服务商的账单也要定期核对,发现异常增长立即排查。只有把短信接口当成一个需要持续投入的安全项目,而不是上线后就再也不管的静态功能,才能真正杜绝恶意调用带来的损失。
