Securing Your Twilio Webhooks in Python

November 22, 2019
Written by

Securing your Twilio Webhooks in Python

Last month a Twitter user described in detail how he almost fell victim to a very credible phishing attempt against his bank account. One of the key aspects of this attempted scam was that the attacker, posing as a bank representative, was able to make the bank send a legitimate verification code to the user’s phone.

While this was just a very clever abuse of the bank’s lax user verification protocols and not a result of a hack, it helps reinforce the idea that you cannot stop thinking about security, and this obviously also applies to the webhooks that support applications that are based on Twilio APIs.

In this article I’m going to discuss the risks that are specific to webhooks and the techniques that you have at your disposal to make them secure.

Understanding the Risks

Before I go into the available solutions, I want to help you understand how an attacker might take advantage of your webhooks if you are not securing them properly.

A webhook is simply a function that is deployed to the Internet behind a public URL. You configure the URLs of your webhooks in the Twilio Console so that Twilio can notify your application of certain events. Have you considered what would happen if someone outside of Twilio obtained those webhook URLs?

Since URLs are publicly reachable on the Internet by anyone, there is really nothing you can do to prevent an attacker from invoking your webhooks in the same way that Twilio does. What’s even more concerning is that the data that Twilio sends to webhooks is publicly documented, so not only the attacker can invoke your Twilio webhooks, but can also populate the web request with fake data that is structured almost like a legitimate invocation from Twilio (the “almost” part is the key to making your webhooks secure).

TwiML Attacks

Many Twilio webhooks return a response that uses the Twilio Markup Language (TwiML), an XML-based language that describes how to respond to the originating event. Consider an application where users can request a password reset by sending an SMS that is responded to with another SMS that includes a reset link. Here is a possible implementation for the webhook in Python:

from flask import Flask, request
from models import User

app = Flask(__name__)


@app.route('/sms/reset', methods='POST'])
def reset():
    sender = request.form.get('From')
    user = User.get_user_from_phone_number(sender)
    temporary_password = user.reset_password()
    resp = MessagingResponse()
    msg = resp.message()
    msg.body(f’Your reset link is https://example.com/reset/{temporary_password}’)
    return str(resp)

Do you see the issue here? If an attacker sends a request to https://example.com/sms/reset with From=+12345678900 as the payload, and the number +1-234-567-8900 happens to be registered to a user, then they will get a TwiML response with a valid reset link that they can use to take control of the user’s account.

Twilio API Attacks

Many webhooks use the Twilio API to carry out actions directly, as opposed to returning TwiML. Once again, if the webhook blindly trusts the arguments in incoming requests, it can be tricked into performing operations on behalf of any user.

Below is a webhook implementation that illustrates this second situation. In this example users can send an SMS to request a new PIN for their account.

import os
from flask import Flask, request
from models import User

app = Flask(__name__)
app.client = Client(
    os.environ['TWILIO_SID'],
    os.environ['TWILIO_AUTH_TOKEN']
)


@app.route('/sms/pin', methods=['POST'])
def pin():
    sender = request.form.get('From')
    user = User.get_user_from_phone_number(sender)
    pin = user.get_new_pin()
    app.client.messages.create(
        body=f'Your PIN is {pin}', 
        from_=os.environ['FROM_NUMBER'],
        to=sender,
    )
    return ''

With this type of webhook the same fake request from the previous section would cause a PIN to be allocated for the user that owns the number +1-234-567-8900 and sent to them by SMS. An attacker who is on the phone with the unsuspecting user could trigger this request and have the user read the PIN back, which is exactly what happened in the bank phishing scam.

Input Data Should Never Be Trusted

What do the two scenarios above have in common? Both have webhooks that make the big mistake of assuming that the input arguments that they receive can be trusted without any validation. If there is one thing you should take away from this article is what is considered to be rule #1 of web application security:

“A web application should never trust input data without validating it first.”

This means that a webhook cannot carry out any actions that depend on data presumably submitted by Twilio in an incoming request before making sure that the data is legitimate.

Validating Twilio Requests

Luckily, there are a couple of measures you can take to completely eliminate the risks of attackers impersonating Twilio and sending malicious requests to your webhooks.

Validate the Twilio Signature

Twilio includes a cryptographic signature in all the requests it sends to webhooks. The signature is given in the X-Twilio-Signature header included in the HTTP request. If you are interested in the gory details, the signing algorithm is described in the Validating Requests are coming from Twilio section of the documentation.

To sign requests, Twilio uses your account’s Auth Token as a signing key. This is great, because the Auth Token is effectively a shared secret between Twilio and you. You can find the Auth Token associated with your account in your Twilio Console:

How to find the Auth Token in the Twilio Console

To verify that a request made to your webhook is legitimate, you must recalculate the signature and compare it against the one sent by Twilio. If the two signatures match, then you can be sure that the request comes from Twilio, because an attacker would not be able to sign the request with a valid signature without knowing your Auth Token.

To make it easy to verify these signatures you can use the RequestValidator class included in the Twilio Helper library for Python. You can initialize this class by passing the Auth Token:

from twilio.request_validator import RequestValidator
validator = RequestValidator(os.environ.get('TWILIO_AUTH_TOKEN'))

To validate a signature you call the validate() method:

signature_is_valid = validator.validate(url, payload, signature)

The three arguments to the validate() method are the URL of the request, a dictionary with the input values, and the signature submitted by Twilio in the X-Twilio-Signature header. The method returns True if the Twilio signature is valid or False otherwise. If this method returns False then you have to assume the request is fake and exit immediately.

Here is how the first SMS webhook example can be secured with signature validation:

import os
from flask import Flask, request, abort
from twilio.request_validator import RequestValidator
from models import User

app = Flask(__name__)


@app.route('/sms/reset', methods=['POST'])
def reset():
    validator = RequestValidator(os.environ.get('TWILIO_AUTH_TOKEN'))
    if not validator.validate(request.url, request.form, request.headers.get('X-Twilio-Signature')):
        abort(400)
    sender = request.form.get('From')
    user = User.get_user_from_phone_number(sender)
    temporary_password = user.reset_password()
    resp = MessagingResponse()
    msg = resp.message()
    msg.body(f’Your reset link is https://example.com/reset/{temporary_password}’)
    return str(resp)

If you use a different framework than Flask you will need to make some minor changes in how you provide the arguments to the validator and how you exit the webhook in case of an invalid signature. For this example I’m exiting using Flask’s abort() function and setting a status code of 400, which is the code for a “bad request” error.

Use Secure HTTP

While signature validation is the most important thing you can do to ensure that requests coming into your webhook are legitimate, it is also important to consider that if your webhook is hosted on a http:// URL then requests sent by Twilio are not going to be encrypted and could be intercepted.

Verifying the signature eliminates any possibility of intercepted requests being changed, but an intercepted request could be captured and then resent by the attacker, a vulnerability known as a replay attack.

To prevent requests from being intercepted in transit you have to use end-to-end encryption, and this is done by hosting your webhooks on https:// endpoints, where all the traffic is encrypted using TLS and the identity of your server can be verified through a SSL certificate. The procedure to add encryption varies across web servers, so for this task I recommend that you search for a tutorial for your stack. If you are using Python and the Flask framework, you can follow my Running your Flask Application over HTTPS tutorial.

Conclusion

My goal for this article is that it helps you evaluate the security of your Twilio webhooks. As you have seen above, the procedure to achieve good security is not very complicated, so I hope you will apply my recommendations to any of your webhooks that are insecure or partially secured.