Skip to content

fix(security): add Twilio webhook signature validation to prevent unauthenticated RCE#7904

Closed
entropidelic wants to merge 6 commits into
mainfrom
twilio-auth-fix
Closed

fix(security): add Twilio webhook signature validation to prevent unauthenticated RCE#7904
entropidelic wants to merge 6 commits into
mainfrom
twilio-auth-fix

Conversation

@entropidelic

@entropidelic entropidelic commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

The SMS adapter's inbound webhook handler (/webhooks/twilio) performs zero authentication on incoming requests. Since the official docs instruct users to expose the webhook to the internet (directly or via ngrok/cloudflared), any attacker who can reach the endpoint can inject forged SMS messages that the agent processes with full tool access, including terminal execution (unauthenticated RCE).

We confirmed this manually: a single curl with a forged payload caused the agent to create an arbitrary file on disk via the terminal tool.
This PR adds X-Twilio-Signature validation (HMAC-SHA1) using stdlib-only crypto (hmac, hashlib, base64), so no new dependencies. The implementation is fail-closed: the adapter refuses to start without SMS_WEBHOOK_URL configured, with an explicit opt-out (SMS_INSECURE_NO_SIGNATURE=true) for local development.

Related Issue

Fixes #7089

Type of Change

  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)

⚠️ Breaking Changes

This PR introduces an intentional breaking change for SMS adapter users. On upgrade, the SMS adapter will refuse to start unless one of the following is set:

  1. SMS_WEBHOOK_URL — the public URL configured in your Twilio console (e.g. https://example.com/webhooks/twilio). This enables Twilio signature validation. Recommended.
  2. SMS_INSECURE_NO_SIGNATURE=true — disables signature validation with a loud warning. For local development only. Not recommended for production.

Why this is necessary: Without this change, every existing and new SMS deployment is silently vulnerable to unauthenticated remote code execution. The previous behavior (no validation, no warning) cannot remain the default.

Migration effort is minimal: Every SMS user already knows their webhook URL — they configured it in the Twilio console. Setting SMS_WEBHOOK_URL to that same value is a one-time change.

Changes Made

  • gateway/platforms/base.py — Added is_network_accessible() as a shared utility (moved from api_server.py), added ipaddress and socket imports
  • gateway/platforms/api_server.py — Removed local _is_network_accessible(), imports from base instead, removed unused ipaddress import
  • gateway/platforms/sms.py:
    • Added hashlib, hmac imports and is_network_accessible import from base
    • Added DEFAULT_WEBHOOK_HOST constant and SMS_WEBHOOK_HOST / SMS_WEBHOOK_URL /SMS_INSECURE_NO_SIGNATURE env vars
    • Added fail-closed startup guard: refuses to start without SMS_WEBHOOK_URL (unless explicitly opted out)
    • Added _validate_twilio_signature(), _check_signature(), and _port_variant_url() methods implementing Twilio's HMAC-SHA1 signature algorithm with timing-safe comparison
      • Port variant handling toggles default ports only (443/80) — non-standard ports are never modified, fixing a bug present in PR fix(security): add Twilio signature validation to SMS webhook #6354
      • Added signature validation in _handle_webhook() — returns 403 for missing or invalid signatures
      • Fixed parse_qs to use keep_blank_values=True (prevents signature mismatch when Twilio sends empty fields)
      • Made bind address configurable via SMS_WEBHOOK_HOST
  • tests/gateway/test_sms.py — Added 22 new tests across 4 test classes: TestWebhookHostConfig (4), TestStartupGuard (3), TestTwilioSignatureValidation (10), TestWebhookSignatureEnforcement (5)
  • website/docs/user-guide/messaging/sms.md — Added SMS_WEBHOOK_URL to Step 3, updated Step 4 startup output, added 3 new env vars to table, added webhook signature validation to Security section
  • website/docs/reference/environment-variables.md — Added SMS_WEBHOOK_URL, SMS_WEBHOOK_HOST, SMS_INSECURE_NO_SIGNATURE entries

How to Test

  1. Verify fail-closed behavior: Start the gateway with SMS enabled but without SMS_WEBHOOK_URL: TWILIO_ACCOUNT_SID=ACtest TWILIO_AUTH_TOKEN=tok TWILIO_PHONE_NUMBER=+15550000000 hermes gateway
  2. Expected: adapter refuses to start with:
    [sms] Refusing to start: SMS_WEBHOOK_URL is required for Twilio signature validation.
  3. Verify insecure opt-out: Add SMS_INSECURE_NO_SIGNATURE=true:
    SMS_INSECURE_NO_SIGNATURE=true hermes gateway
  4. Expected: adapter starts with warning, forged curl returns HTTP 200 (vulnerable, as intended for dev).
  5. Verify signature validation: Set SMS_WEBHOOK_URL=https://your-domain/webhooks/twilio and restart. A forged curl without X-Twilio-Signature returns HTTP 403:
curl -X POST http://127.0.0.1:8080/webhooks/twilio \
  -d 'From=%2B15551234567&To=%2B15550000000&Body=forged&MessageSid=SMfake'
  1. Expected: HTTP 403 and log [sms] Rejected: missing X-Twilio-Signature header
  2. Run tests:
pytest tests/gateway/test_sms.py -v        # 43 pass
pytest tests/gateway/test_api_server.py -v  # 106 pass (refactor check)

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Supply Chain Risk Detected

This PR contains patterns commonly associated with supply chain attacks. This does not mean the PR is malicious — but these patterns require careful human review before merging.

⚠️ WARNING: base64 encoding/decoding detected

Base64 has legitimate uses (images, JWT, etc.) but is also commonly used to obfuscate malicious payloads. Verify the usage is appropriate.

Matches (first 20):

573:+        computed = base64.b64encode(mac.digest()).decode("utf-8")
2055:+    return base64.b64encode(mac.digest()).decode("utf-8")
3006:+        image_base64 = base64.b64encode(image_data).decode("ascii")

Automated scan triggered by supply-chain-audit. If this is a false positive, a maintainer can approve after manual review.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Supply Chain Risk Detected

This PR contains patterns commonly associated with supply chain attacks. This does not mean the PR is malicious — but these patterns require careful human review before merging.

⚠️ WARNING: base64 encoding/decoding detected

Base64 has legitimate uses (images, JWT, etc.) but is also commonly used to obfuscate malicious payloads. Verify the usage is appropriate.

Matches (first 20):

573:+        computed = base64.b64encode(mac.digest()).decode("utf-8")
2055:+    return base64.b64encode(mac.digest()).decode("utf-8")
3006:+        image_base64 = base64.b64encode(image_data).decode("ascii")

Automated scan triggered by supply-chain-audit. If this is a false positive, a maintainer can approve after manual review.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Supply Chain Risk Detected

This PR contains patterns commonly associated with supply chain attacks. This does not mean the PR is malicious — but these patterns require careful human review before merging.

⚠️ WARNING: base64 encoding/decoding detected

Base64 has legitimate uses (images, JWT, etc.) but is also commonly used to obfuscate malicious payloads. Verify the usage is appropriate.

Matches (first 20):

565:+        computed = base64.b64encode(mac.digest()).decode("utf-8")
2047:+    return base64.b64encode(mac.digest()).decode("utf-8")
2998:+        image_base64 = base64.b64encode(image_data).decode("ascii")

Automated scan triggered by supply-chain-audit. If this is a false positive, a maintainer can approve after manual review.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Supply Chain Risk Detected

This PR contains patterns commonly associated with supply chain attacks. This does not mean the PR is malicious — but these patterns require careful human review before merging.

⚠️ WARNING: base64 encoding/decoding detected

Base64 has legitimate uses (images, JWT, etc.) but is also commonly used to obfuscate malicious payloads. Verify the usage is appropriate.

Matches (first 20):

565:+        computed = base64.b64encode(mac.digest()).decode("utf-8")
2047:+    return base64.b64encode(mac.digest()).decode("utf-8")
3008:+        image_base64 = base64.b64encode(image_data).decode("ascii")

Automated scan triggered by supply-chain-audit. If this is a false positive, a maintainer can approve after manual review.

@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #7933. Your commits were cherry-picked onto current main with your authorship preserved in git log. Thanks for the thorough implementation, @entropidelic — fail-closed design, port variant handling, and the keep_blank_values fix were all excellent.

@teknium1 teknium1 closed this Apr 11, 2026
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]: Unauthenticated Remote Code Execution via SMS Webhook — Missing Twilio Signature Validation

2 participants