Skip to content

fix: validate webhook signatures on percent-encoded request paths - #809

Merged
manisha1997 merged 2 commits into
mainfrom
fix/webhook-auth-encoded-path-bypass
Sep 10, 2026
Merged

fix: validate webhook signatures on percent-encoded request paths#809
manisha1997 merged 2 commits into
mainfrom
fix/webhook-auth-encoded-path-bypass

Conversation

@manisha1997

Copy link
Copy Markdown
Contributor

Problem

Rack::TwilioWebhookAuthentication decided whether to validate a request by matching the configured
patterns against the raw PATH_INFO, which is still percent-encoded. The routers this middleware
sits in front of — Rails' Journey, Sinatra — decode and normalize the path before matching their own
routes, so the two disagreed about which path had been requested.

On a disagreement the middleware failed open: it passed the request to the app without validating
its signature, while the router still dispatched it to the guarded action.

Fix

Match the configured patterns against each form the path can take by the time it reaches the router
— raw, percent-decoded, and with dot segments and repeated slashes collapsed — and validate if any
of them matches.

Matching on any candidate is deliberate: it can only widen the set of validated requests, so an
integration that works today cannot silently lose its protection. The failure mode inverts from
"wave it through" to "check it".

Three details worth a reviewer's attention:

  • Rack::Utils.unescape_path, not unescape. The latter is form decoding and turns + into a
    space; in a path a plus is a literal plus.
  • Invalid UTF-8 is scrubbed. %FF%FE decodes to bytes that make String#match raise
    ArgumentError, which would be an unhandled 500 on a hostile path.
  • Single decode, not repeated. This matches what Rails actually does — %256F decodes to a
    literal %6F and routes to /%6Fice, not /voice — so decoding twice would over-match.

original_url, used for the signature computation itself, is deliberately untouched. It has a
related encoding mismatch, but changing what gets signed risks breaking integrations that work
today, and it is unreachable while the path gate holds. Worth its own decision.

Tests

10 regression specs covering percent-encoded paths, encoded slashes, dot segments against an
anchored pattern, undecodable bytes, literal +, and the negative cases that must still pass
through unvalidated.

Full suite green (377 examples, 0 failures); Rubocop clean on both files.

Notes

Tracked internally as SECOPS-25944. Affects current releases, not only the version named in the
report. Maintainers may want to consider a CVE and a GitHub Security Advisory alongside the release
that carries this.

🤖 Generated with Claude Code

manisha1997 and others added 2 commits September 7, 2026 14:21
Rack::TwilioWebhookAuthentication decided whether to validate a request
by matching the configured patterns against the raw PATH_INFO, which is
still percent-encoded. The routers this middleware sits in front of
(Rails, Sinatra) decode and normalize the path before matching their own
routes, so the two disagreed about which path had been requested.

On a disagreement the middleware failed open: a request to /%76oice did
not match /\/voice/, so signature validation was skipped entirely and the
request was passed through to an app that then routed it to the /voice
action. Any endpoint protected only by this middleware would accept
unsigned, forged webhooks, with no 403 and no log line to show for it.

Match the configured patterns against each form the path can take by the
time it reaches the router -- raw, percent-decoded, and with dot segments
and repeated slashes collapsed -- and validate if any of them matches.
Erring towards validating means this can only widen the set of checked
requests, so an integration that works today cannot silently lose its
protection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@manisha1997

Copy link
Copy Markdown
Contributor Author

@manisha1997
manisha1997 merged commit c87a7b4 into main Sep 10, 2026
14 of 16 checks passed
@manisha1997
manisha1997 deleted the fix/webhook-auth-encoded-path-bypass branch September 10, 2026 05:08
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.

2 participants