Senior Application Developer

loc: Maynooth, ON K0L 2S0tel: 613-553-0960email: rgsamways@gmail.com
Robin Samways

Farpost · Notifications

$ When the Infrastructure Lies About Its Own URL

Twilio signs every webhook request with the exact URL it sent that request to, and Farpost validates that signature to make sure a request claiming to be from Twilio actually is. In production, every single validation failed.

The cause was invisible locally: Railway, the hosting platform, terminates HTTPS at its own proxy, so the request arrives at the app internally as plain http://. But Twilio signed the https:// URL it actually called. Validating the signature against the URL the app sees, instead of the URL Twilio used, meant every real request failed a check it should have passed.

def _validate_twilio_signature(request: Request, body: dict) -> None:
    """Validate Twilio request signature to prevent spoofed webhook calls."""
    validator = RequestValidator(settings.TWILIO_AUTH_TOKEN)
    signature = request.headers.get("X-Twilio-Signature", "")
    url = str(request.url)
    # Railway terminates TLS at the proxy — internal requests arrive as http,
    # but Twilio signed the https:// URL it actually sent to.
    if url.startswith("http://"):
        url = "https://" + url[7:]

    if not validator.validate(url, body, signature):
        raise HTTPException(status_code=403, detail={"code": "INVALID_SIGNATURE"})

The fix

Before validating, the URL's scheme is rewritten from http:// to https:// if it starts with http:// — matching what Twilio actually signed, without touching anything about how the signature itself is checked.

Why this matters

This class of bug is invisible in local development, where there's no TLS termination happening in front of the app at all, and only shows up once the real infrastructure is in the loop — it can't be found by reading code or running tests locally, only by actually deploying and testing against the real thing. Catching and fixing it is a direct demonstration of root-cause diagnosis under a genuinely confusing symptom: a security check failing 100% of the time, for a reason that had nothing to do with the security logic itself.

Feedback on this page