Skip to content

Fix Twilio Signature Validation in Cloudflare Worker #8

@mikegehard

Description

@mikegehard

Problem

Twilio webhook signature validation is failing in the Cloudflare Worker environment despite passing local tests. This is causing all incoming MMS messages to be rejected as invalid.

Current Implementation

// We construct the validation string from URL and form parameters
const url = new URL(request.url);
const isValid = validateTwilioRequest(
    url.toString(),
    formEntries,
    request.headers.get('X-Twilio-Signature') || '',
    env.TWILIO_AUTH_TOKEN,
);

Logs

Two failed validation attempts from production:

POST https://tree-spotter.mike-gehard.workers.dev/sms - Ok @ 5/25/2025, 11:58:21 PM
[2025-05-25T23:58:21.577Z] Incoming request to /sms
... all parameters logged correctly ...
Signature:  zHbKAIKAkQjLSNyoqt9XtHlIB7o=
URL:  https://tree-spotter.mike-gehard.workers.dev/sms
(warn) Invalid Twilio signature received

POST https://tree-spotter.mike-gehard.workers.dev/sms - Ok @ 5/26/2025, 12:06:41 AM
[2025-05-26T00:06:41.108Z] Incoming request to /sms
... all parameters logged correctly ...
Signature:  EMxxdUIUJrgE5pSnhL66qG+za5c=
URL:  https://tree-spotter.mike-gehard.workers.dev/sms
(warn) Invalid Twilio signature received

Analysis

The second request matches our test case exactly but still fails in production. Key observations:

  1. The tests pass locally using Node.js
  2. The validation fails consistently in Cloudflare Workers
  3. The same request data that works in tests fails in production

Potential Causes

  1. URL Normalization

    • Cloudflare might be handling the URL differently than our local environment
    • The URL object in Cloudflare Workers might have subtle differences
    • Query parameters or trailing slashes might be handled differently
  2. Parameter Order

    • While we sort the parameters alphabetically, there might be encoding differences
    • Form data might be processed differently in Cloudflare Workers vs Node.js
  3. Crypto Implementation

    • Cloudflare Workers uses WebCrypto API while our local tests use Node's crypto
    • There might be subtle differences in how HMAC-SHA1 is implemented

Next Steps

  1. Add Debug Logging:
// Add to validateTwilioRequest:
console.log('Data being signed:', dataToSign);
console.log('Generated signature:', expectedSignature);
  1. Try URL Normalization:
// Consider using just the pathname
const urlForSigning = new URL(request.url).pathname;
  1. Compare with Official Implementation:

    • Review Twilio's official validation code
    • Consider using @twilio/runtime-handler if compatible with Cloudflare Workers
  2. Test Different URL Formats:

    • Test with/without trailing slash
    • Test with different URL normalizations
    • Compare URL handling between Node.js and Cloudflare Workers

Related Documentation

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecurity

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions