Skip to content

fix: validate the received url before its re-encoded variants - #1194

Open
dirnalafeinte wants to merge 1 commit into
twilio:mainfrom
dirnalafeinte:fix/validate-request-preserve-url-encoding
Open

dirnalafeinte wants to merge 1 commit into
twilio:mainfrom
dirnalafeinte:fix/validate-request-preserve-url-encoding

Conversation

@dirnalafeinte

Copy link
Copy Markdown

Fixes #1183

validateRequest() rebuilds every candidate URL from new URL(url), so the string it hashes is never the string Twilio actually signed. Two separate normalizations get in the way:

  • new URL() percent-encodes characters the back end leaves as-is, so ' becomes %27
  • withLegacyQuerystring() round-trips through querystring.parse/stringify, which decodes + to a space and re-encodes it as %20

Each of the four existing variants goes through at least one of those, so a URL whose query contains both a space encoded as + and a single quote cannot be reproduced by any of them, and a validly signed request is rejected.

The existing single-quote tests pass only by coincidence: Node's querystring.escape does not escape ', so the legacy-querystring variant happens to reproduce the raw string. Add a + to the same query and that coincidence disappears — which is exactly the ?name=William+O'hara case in #1183.

Change

Check the URL exactly as received first, since that is the string Twilio signed, followed by a port-toggled form of it. The port toggle is done with string surgery on the authority component only (handling userinfo@ and IPv6 literals), so the path and query keep their exact bytes rather than being rebuilt from a parsed object.

The four existing variants are left untouched and still run afterwards, so URLs that only validate in a normalized form keep working. The change is purely additive — it accepts signatures that are already valid, and every candidate is still verified against the auth token through the same constant-time comparison.

Testing

Added three cases covering ?name=William+O'hara — identical URL, target with the port, and target without the port. All three fail on main and pass with this change.

Tests:       432 passed, 432 total
Test Suites: 28 passed, 28 total

npm run test:typescript (tsc --noEmit) and prettier --check are both clean.

Note: PULL_REQUEST_TEMPLATE.md asks to run npm run webhook-test for Request Validation changes, but no such script exists in package.json. The equivalent spec/cluster/webhook.spec.ts needs live credentials (TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_API_KEY, TWILIO_API_SECRET) plus localtunnel and makes real API calls, so I did not run it. Happy to if a maintainer can run it against CI credentials.

Checklist

  • I acknowledge that all my contributions will be made under the project's license
  • I have made a material change to the repo (functionality, testing, spelling, grammar)
  • I have read the Contribution Guidelines and my PR follows them
  • I have titled the PR appropriately
  • I have updated my branch with the main branch
  • I have added tests that prove my fix is effective or that my feature works
  • I have added the necessary documentation about the functionality in the appropriate .md file — no public API or behaviour change to document; this restores documented behaviour
  • I have added inline documentation to the code I modified

`validateRequest()` rebuilt every candidate url from `new URL(url)`, so the
string it hashed was never the string Twilio signed. `new URL()` percent-encodes
characters the back end leaves as-is (`'` becomes `%27`), and the legacy
querystring round trip rewrites `+` as `%20`, so a url whose query contains both
a space encoded as `+` and a single quote could not be reproduced by any of the
four variants and validation failed on a validly signed request.

Check the received url as-is first, plus a port-toggled form built by string
surgery on the authority only, so the path and query keep their exact bytes. The
four existing variants are left untouched and still run afterwards, so urls that
only validate in a normalized form keep working.

Fixes twilio#1183

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] validateRequest() rejects valid signatures containing apostrophes (regression v5.0.4)

1 participant