SMS is the most expensive metered communication channel in a typical web stack — Twilio charges $0.0079–$0.02 per outbound message, and there is no free tier for abuse. An SMS route without per-IP rate limiting lets an attacker trigger thousands of messages per minute, reaching four-digit charges before any alert fires. CWE-799 (Improper Control of Interaction Frequency) is the direct mapping, alongside OWASP A05:2021. Beyond direct billing, carriers flag high-volume spammy senders for grey-listing or number suspension, destroying your transactional SMS deliverability for legitimate users. This is a cost bomb that also breaks a security-critical channel (OTP delivery).
High because SMS costs are metered per message with no platform-level cap, making a missing rate limit an immediately exploitable financial attack vector.
Add per-IP rate limiting at the route level before the Twilio client is called. Three sends per hour per IP is generous for legitimate OTP flows and stops abuse cold.
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
import twilio from 'twilio'
const limiter = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.fixedWindow(3, '1 h'),
})
const client = twilio(process.env.TWILIO_SID!, process.env.TWILIO_AUTH_TOKEN!)
export async function POST(req: Request) {
const ip = req.headers.get('x-forwarded-for') ?? 'anon'
const { success } = await limiter.limit(`sms:${ip}`)
if (!success) return Response.json({ error: 'Rate limited' }, { status: 429 })
const { to, body } = SMSSchema.parse(await req.json())
await client.messages.create({ to, from: process.env.TWILIO_FROM!, body })
return Response.json({ sent: true })
}
ID: ai-slop-cost-bombs.unrate-limited-spend.sms-rate-limited
Severity: high
What to look for: When an SMS library is in package.json dependencies (twilio, @aws-sdk/client-sns, messagebird, vonage, @plivo/server-sdk, bandwidth-rest-api), walk source files for SMS send call sites: .messages.create(, .sendMessage(, client.messages.create(. Count all SMS send call sites and verify each is preceded by a rate-limit check OR is inside a route handler that imports a rate limiter.
Pass criteria: 100% of SMS send call sites are either gated by a rate limiter OR are part of a transactional flow (1 SMS per user action). Report: "X SMS send call sites inspected, Y rate-controlled, 0 unbounded."
Fail criteria: At least 1 SMS send call has no upstream rate limiter.
Skip (N/A) when: No SMS library in dependencies.
Detail on fail: "1 unbounded SMS handler: app/api/sms/route.ts calls twilio.messages.create() with no rate limit — at $0.01/SMS, an attacker can run up $1,000+ in minutes"
Remediation: SMS is even more expensive than email. A small rate limit per IP is mandatory:
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const limiter = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.fixedWindow(3, '1 h'), // 3 SMS per hour per IP
})
export async function POST(req: Request) {
const ip = req.headers.get('x-forwarded-for') ?? 'anon'
const { success } = await limiter.limit(ip)
if (!success) return Response.json({ error: 'Rate limited' }, { status: 429 })
await twilio.messages.create({ to: ..., from: ..., body: ... })
return Response.json({ sent: true })
}