Twilio Integration Security Guide

Share

To secure Twilio integration, you need to: (1) validate webhook signatures using validateRequest() on all incoming Twilio webhooks, (2) implement rate limiting by phone number AND IP to prevent SMS pumping fraud, (3) store credentials (Account SID, Auth Token) in environment variables only, (4) configure geographic permissions in Twilio console to block high-risk regions, and (5) use HTTPS for all webhook URLs. This blueprint prevents costly abuse of your SMS/voice infrastructure.

Setup Time1-2 hours

TL;DR

Twilio webhooks must be validated to prevent spoofing. Use validateRequest() for all incoming webhooks, implement rate limiting to prevent SMS pumping fraud, keep credentials server-side only, and use geographic permissions to limit abuse.

Webhook Validation Twilio

app/api/twilio/sms/route.ts
import twilio from 'twilio'

const authToken = process.env.TWILIO_AUTH_TOKEN!

export async function POST(req: Request) {
  const url = req.url
  const signature = req.headers.get('x-twilio-signature')!

  const formData = await req.formData()
  const params: Record<string, string> = {}
  formData.forEach((value, key) => {
    params[key] = value.toString()
  })

  // Validate the request came from Twilio
  const isValid = twilio.validateRequest(authToken, signature, url, params)

  if (!isValid) {
    console.error('Invalid Twilio webhook signature')
    return new Response('Forbidden', { status: 403 })
  }

  // Process the verified webhook
  const from = params.From
  const body = params.Body

  console.log(`SMS from ${from}: ${body}`)

  // Return TwiML response
  const twiml = new twilio.twiml.MessagingResponse()
  twiml.message('Thanks for your message!')

  return new Response(twiml.toString(), {
    headers: { 'Content-Type': 'text/xml' },
  })
}

Sending SMS Securely

lib/twilio.ts
import twilio from 'twilio'

const client = twilio(
  process.env.TWILIO_ACCOUNT_SID!,
  process.env.TWILIO_AUTH_TOKEN!
)

export async function sendVerificationCode(
  phoneNumber: string,
  code: string
) {
  // Rate limit before sending
  const recentAttempts = await getRecentAttempts(phoneNumber)
  if (recentAttempts > 3) {
    throw new Error('Too many verification attempts')
  }

  await client.messages.create({
    body: `Your verification code is: ${code}`,
    from: process.env.TWILIO_PHONE_NUMBER!,
    to: phoneNumber,
  })

  await recordAttempt(phoneNumber)
}

SMS Pumping Prevention

lib/sms-protection.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(3, '1 h'),  // 3 per hour
  analytics: true,
})

export async function canSendSms(phoneNumber: string, ip: string) {
  // Rate limit by phone number
  const phoneLimit = await ratelimit.limit(`sms:phone:${phoneNumber}`)
  if (!phoneLimit.success) return false

  // Rate limit by IP
  const ipLimit = await ratelimit.limit(`sms:ip:${ip}`)
  if (!ipLimit.success) return false

  // Block high-risk country codes (configure based on your needs)
  const blockedPrefixes = ['+882', '+883']  // Satellite phones
  if (blockedPrefixes.some(p => phoneNumber.startsWith(p))) {
    return false
  }

  return true
}

SMS pumping is expensive. Attackers can trigger thousands of SMS messages to premium-rate numbers. Always rate limit by phone number AND IP, and configure geographic permissions in your Twilio console.

Security Checklist

Pre-Launch Checklist

Webhook signatures validated

Credentials stored in environment variables

Rate limiting implemented

Geographic permissions configured

Webhook URL uses HTTPS

SendGrid Email Integration Stripe Webhook Patterns Edge Rate Limiting

Check Your Twilio Integration

Scan for webhook and credential issues.

Start Free Scan
Security Blueprints

Twilio Integration Security Guide