twilio / twilio-ruby

A Ruby gem for communicating with the Twilio API and generating TwiML
MIT License
1.35k stars 462 forks source link

fix: Validate signatures in Rack middleware for non-form-data payloads #590

Closed gabrielg closed 2 years ago

gabrielg commented 2 years ago

The webhook authentication middleware has been using the return value of request.POST to determine the params to validate, assuming it returns a non-Hash in the case of a non-formish-POST. However, this is not true, though perhaps it was at some point in the past:

https://github.com/rack/rack/blob/8c9a97fe6073a8f4c17f85fff20f20eae22d1bcb/lib/rack/request.rb#L451-L478

This means the RequestValidator body validation code is entirely skipped over, since params_hash is always Enumerable:

https://github.com/twilio/twilio-ruby/blob/6c50e0fa13bfefdcdaaa0799b8798023ce492e60/lib/twilio-ruby/security/request_validator.rb#L34-L39

So the body is always considered valid, and only the URL signature is ever validated:

https://github.com/twilio/twilio-ruby/blob/6c50e0fa13bfefdcdaaa0799b8798023ce492e60/lib/twilio-ruby/security/request_validator.rb#L32 https://github.com/twilio/twilio-ruby/blob/6c50e0fa13bfefdcdaaa0799b8798023ce492e60/lib/twilio-ruby/security/request_validator.rb#L46

This fixes the problem by being strict about what's considered a form, and actually reading the body as the params in the case the middleware has not received a form.

I don't actually know how Twilio calculates signatures for multipart/form-data requests, if it ever makes them at all, so these aren't treated any differently than any other non application/x-www-form-urlencoded body.


Proof of Concept

gabrielg/twilio-ruby-signature-issue is a Sinatra app I prepared earlier. main has the broken code, fixed vendors this branch.

I needed a JSON payload, so I set up an Event Streams sink and subscription and sent a message to a number. Here's what that looks like, with the from and to numbers redacted for privacy:

2022-01-20T04:21:12.848949+00:00 app[web.1]: I, [2022-01-20T04:21:12.848877 #4]  INFO -- : Request signature: Z8VaIO3lzXlpt4Wo2+1cLewPNHc=
2022-01-20T04:21:12.848971+00:00 app[web.1]: I, [2022-01-20T04:21:12.848949 #4]  INFO -- : Request body: "[{\"specversion\":\"1.0\",\"type\":\"com.twilio.messaging.inbound-message.received\",\"source\":\"/2010-04-01/Accounts/ACd19cd7a2e1dd2459a0134a1045a8ad22/Messages/SMb7f53fae9b4e2f7934db56a85dc380a6.json\",\"id\":\"EZ0b589f28245712b627d6281e12452ca4\",\"dataschema\":\"https://events-schemas.twilio.com/Messaging.InboundMessageV1/1\",\"datacontenttype\":\"application/json\",\"time\":\"2022-01-20T04:21:12.000Z\",\"data\":{\"fromState\":\"IL\",\"eventName\":\"com.twilio.messaging.inbound-message.received\",\"body\":\"test \",\"numMedia\":0,\"toZip\":\"35160\",\"timestamp\":\"2022-01-20T04:21:12.000Z\",\"fromCity\":\"CHICAGO\",\"accountSid\":\"ACd19cd7a2e1dd2459a0134a1045a8ad22\",\"to\":\"+[REDACTED]\",\"toCountry\":\"TALLADEGA\",\"toState\":\"AL\",\"toCity\":\"TALLADEGA\",\"from\":\"+[REDACTED]\",\"fromCountry\":\"US\",\"numSegments\":1,\"messageSid\":\"SMb7f53fae9b4e2f7934db56a85dc380a6\",\"fromZip\":\"60603\"}}]"
2022-01-20T04:21:12.849007+00:00 app[web.1]: I, [2022-01-20T04:21:12.848985 #4]  INFO -- : Request URL: https://twilio-ruby-poc.herokuapp.com/incoming?bodySHA256=afa6016db35c4065e97fcbbd50263823ab6a02a8041b406876c5ac87078ced90
2022-01-20T04:21:12.849023+00:00 app[web.1]: I, [2022-01-20T04:21:12.849005 #4]  INFO -- : Request media type: application/json
2022-01-20T04:21:12.849165+00:00 app[web.1]: 54.234.178.255 - - [20/Jan/2022:04:21:12 +0000] "POST /incoming?bodySHA256=afa6016db35c4065e97fcbbd50263823ab6a02a8041b406876c5ac87078ced90 HTTP/1.1" 200 31 0.0008

I can take that signature and URL, set the Content-Type to application/json, and curl anything and have it pass signature validation:

$ curl -X POST -H "X-Twilio-Signature: Z8VaIO3lzXlpt4Wo2+1cLewPNHc=" -H "Content-Type: application/json" -d "does not matter" https://twilio-ruby-poc.herokuapp.com/incoming\?bodySHA256\=afa6016db35c4065e97fcbbd50263823ab6a02a8041b406876c5ac87078ced90
Signature validation succeeded.

A bad actor thus only needs a valid signature and URL to make requests, not the original auth token used for signing.

I also set up another sink and subscription against a Heroku app running the fixed branch. An incoming message against it looks like:

2022-01-20T04:21:12.851310+00:00 app[web.1]: I, [2022-01-20T04:21:12.851254 #4]  INFO -- : Request signature: 17BMAnL0duOg0piNC/D2Xx5BWiE=
2022-01-20T04:21:12.851341+00:00 app[web.1]: I, [2022-01-20T04:21:12.851316 #4]  INFO -- : Request body: "[{\"specversion\":\"1.0\",\"type\":\"com.twilio.messaging.inbound-message.received\",\"source\":\"/2010-04-01/Accounts/ACd19cd7a2e1dd2459a0134a1045a8ad22/Messages/SMb7f53fae9b4e2f7934db56a85dc380a6.json\",\"id\":\"EZ0b589f28245712b627d6281e12452ca4\",\"dataschema\":\"https://events-schemas.twilio.com/Messaging.InboundMessageV1/1\",\"datacontenttype\":\"application/json\",\"time\":\"2022-01-20T04:21:12.000Z\",\"data\":{\"fromState\":\"IL\",\"eventName\":\"com.twilio.messaging.inbound-message.received\",\"body\":\"test \",\"numMedia\":0,\"toZip\":\"35160\",\"timestamp\":\"2022-01-20T04:21:12.000Z\",\"fromCity\":\"CHICAGO\",\"accountSid\":\"ACd19cd7a2e1dd2459a0134a1045a8ad22\",\"to\":\"+[REDACTED]\",\"toCountry\":\"TALLADEGA\",\"toState\":\"AL\",\"toCity\":\"TALLADEGA\",\"from\":\"+[REDACTED]\",\"fromCountry\":\"US\",\"numSegments\":1,\"messageSid\":\"SMb7f53fae9b4e2f7934db56a85dc380a6\",\"fromZip\":\"60603\"}}]"
2022-01-20T04:21:12.851410+00:00 app[web.1]: I, [2022-01-20T04:21:12.851386 #4]  INFO -- : Request URL: https://obscure-citadel-66169.herokuapp.com/incoming?bodySHA256=afa6016db35c4065e97fcbbd50263823ab6a02a8041b406876c5ac87078ced90
2022-01-20T04:21:12.851431+00:00 app[web.1]: I, [2022-01-20T04:21:12.851413 #4]  INFO -- : Request media type: application/json
2022-01-20T04:21:12.851616+00:00 app[web.1]: 54.172.192.162 - - [20/Jan/2022:04:21:12 +0000] "POST /incoming?bodySHA256=afa6016db35c4065e97fcbbd50263823ab6a02a8041b406876c5ac87078ced90 HTTP/1.1" 200 31 0.0009

If I take that signature and URL and attempt to tamper with the body, validation now fails, as expected:

curl -X POST -H "X-Twilio-Signature: 17BMAnL0duOg0piNC/D2Xx5BWiE=" -H "Content-Type: application/json" -d "does not matter" https://obscure-citadel-66169.herokuapp.com/incoming\?bodySHA256\=afa6016db35c4065e97fcbbd50263823ab6a02a8041b406876c5ac87078ced90
Twilio Request Validation Failed.

FWIW: I tried to disclose this problem and the fix via Bugcrowd but was told that non-validation of webhook signatures is not, in fact, considered a security issue.

Checklist

philnash commented 2 years ago

Hi @gabrielg, thanks so much for this fix. I wrote the original rack middleware here and it was an oversight not testing other content types. I just wanted to add my thanks for your noticing and fixing of this code and for documenting it and for the examples in this PR. ❤️❤️❤️