SMS send routes are rate-limited
Why it matters
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).
Severity rationale
High because SMS costs are metered per message with no platform-level cap, making a missing rate limit an immediately exploitable financial attack vector.
Remediation
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 })
}
Detection
-
ID:
sms-rate-limited -
Severity:
high -
What to look for: When an SMS library is in
package.jsondependencies (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 }) }
External references
- cwe · CWE-770 — Allocation of Resources Without Limits or Throttling
- cwe · CWE-799 — Improper Control of Interaction Frequency
- owasp:2021 · A05
Taxons
History
- 2026-04-18·v1.0.0·Initial import from ai-slop-cost-bombs·automated