Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions skills/x-twitter/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@ python3 skills/x-twitter/x-post.py engagement 2040817066199195818

## Setup

1. Go to https://developer.x.com and sign in
2. Create a Project + App
3. Generate keys and add to `.env`:
1. Install Python dependencies (one-time):
```
pip3 install requests requests-oauthlib
```
2. Go to https://developer.x.com and sign in
3. Create a Project + App
4. Generate keys and add to `.env`:
```
X_API_KEY=...
X_API_SECRET=...
Expand Down
9 changes: 5 additions & 4 deletions skills/x-twitter/x-post.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ def _require_requests():
import requests as _requests
from requests_oauthlib import OAuth1 as _OAuth1
except ImportError:
print("Installing required packages...")
os.system("pip3 install --break-system-packages requests requests-oauthlib")
import requests as _requests
from requests_oauthlib import OAuth1 as _OAuth1
sys.exit(
"x-post: missing dependencies. Install them with:\n"
" pip3 install requests requests-oauthlib\n"
"or add them to your project's requirements."
)
requests = _requests
OAuth1 = _OAuth1

Expand Down
8 changes: 5 additions & 3 deletions src/agent-api.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ def _safe_id(raw: str) -> str:


def validate_twilio_signature(handler, body: str) -> bool:
"""Validate X-Twilio-Signature if TWILIO_AUTH_TOKEN is configured.
Returns True if valid or if token not configured (local dev)."""
"""Validate X-Twilio-Signature against TWILIO_AUTH_TOKEN.
Fails closed — returns False when the token is not configured so that
unauthenticated requests cannot create tasks via the /twilio/* endpoints.
TWILIO_AUTH_TOKEN must be set in .env for these endpoints to accept webhooks."""
auth_token = os.environ.get("TWILIO_AUTH_TOKEN", "")
if not auth_token:
return True
return False
import hmac, hashlib, base64
from urllib.parse import parse_qs

Expand Down
94 changes: 94 additions & 0 deletions tests/agent-api-twilio-auth.test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Security regression: validate_twilio_signature must fail CLOSED when
TWILIO_AUTH_TOKEN is not set.

Finding #2 from the 2026-07-06 source-code security audit: the original
implementation returned True (accept all) when auth_token was empty,
allowing unauthenticated Twilio webhook requests to create tasks in the
agent.

The fix: return False when TWILIO_AUTH_TOKEN is unset so that /twilio/*
endpoints always reject unauthenticated requests. Operators must set the
token in .env for Twilio webhooks to work.

Run: python3 tests/agent-api-twilio-auth.test.py
Exit: 0 on pass, 1 on fail.
"""
from __future__ import annotations

import importlib.util
import os
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock

REPO = Path(__file__).resolve().parent.parent


def _load_agent_api() -> object:
spec = importlib.util.spec_from_file_location("agent_api", REPO / "src" / "agent-api.py")
mod = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(mod)
return mod


class TwilioAuthTests(unittest.TestCase):
def setUp(self):
# Ensure no token in env for the fail-closed tests.
self._orig = os.environ.pop("TWILIO_AUTH_TOKEN", None)
self.mod = _load_agent_api()

def tearDown(self):
if self._orig is not None:
os.environ["TWILIO_AUTH_TOKEN"] = self._orig
else:
os.environ.pop("TWILIO_AUTH_TOKEN", None)

def _make_handler(self, headers: dict | None = None) -> MagicMock:
h = MagicMock()
h.headers = {**(headers or {})}
h.path = "/twilio/voice"
return h

def test_no_token_rejects_request(self):
"""TWILIO_AUTH_TOKEN unset → validate_twilio_signature returns False (fail closed)."""
os.environ.pop("TWILIO_AUTH_TOKEN", None)
handler = self._make_handler()
result = self.mod.validate_twilio_signature(handler, "CallSid=CA123&To=%2B1555")
self.assertFalse(result, "Must reject when TWILIO_AUTH_TOKEN is not configured")

def test_empty_token_rejects_request(self):
"""TWILIO_AUTH_TOKEN='' (explicitly empty) → rejects."""
os.environ["TWILIO_AUTH_TOKEN"] = ""
handler = self._make_handler()
result = self.mod.validate_twilio_signature(handler, "CallSid=CA123")
self.assertFalse(result)

def test_valid_token_with_missing_signature_rejects(self):
"""Token configured but X-Twilio-Signature header absent → rejects."""
os.environ["TWILIO_AUTH_TOKEN"] = "test_token_abc"
handler = self._make_handler({"Host": "example.com"})
result = self.mod.validate_twilio_signature(handler, "CallSid=CA123")
self.assertFalse(result)

def test_valid_token_with_wrong_signature_rejects(self):
"""Token configured but signature doesn't match → rejects."""
os.environ["TWILIO_AUTH_TOKEN"] = "test_token_abc"
handler = self._make_handler({
"Host": "example.com",
"X-Forwarded-Proto": "https",
"X-Twilio-Signature": "badsig",
})
os.environ["TWILIO_WEBHOOK_URL"] = "https://example.com"
try:
result = self.mod.validate_twilio_signature(handler, "CallSid=CA123")
finally:
os.environ.pop("TWILIO_WEBHOOK_URL", None)
self.assertFalse(result)


if __name__ == "__main__":
result = unittest.main(exit=False).result
sys.exit(0 if result.wasSuccessful() else 1)
Loading