From bf3596682032de8f7dbe20531b121a1f6e6c85a2 Mon Sep 17 00:00:00 2001 From: john-the-dev <52230987+john-the-dev@users.noreply.github.com> Date: Tue, 7 Jul 2026 04:16:05 -0700 Subject: [PATCH 1/2] security: fail-closed on Twilio webhook auth + drop x-post auto-install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Finding #2 (agent-api.py):** `validate_twilio_signature` returned True when TWILIO_AUTH_TOKEN was unset, accepting all unauthenticated Twilio webhooks and allowing arbitrary task injection via /twilio/* endpoints. Changed to return False — fail closed — with a 4-test regression suite. **Finding #5 (skills/x-twitter/x-post.py):** `_require_requests` ran `pip3 install --break-system-packages` as an import side-effect, exposing the install to supply-chain attacks on unpinned packages. Replaced with a clear diagnostic `sys.exit` telling the operator to install the deps explicitly. Both findings confirmed in workspace/notes/security-audit-source-2026-07-06.md. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01QB3qZQrwponfum2JFNvHeh --- skills/x-twitter/x-post.py | 9 +-- src/agent-api.py | 8 ++- tests/agent-api-twilio-auth.test.py | 94 +++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 tests/agent-api-twilio-auth.test.py diff --git a/skills/x-twitter/x-post.py b/skills/x-twitter/x-post.py index d002a0d61..2891d278d 100644 --- a/skills/x-twitter/x-post.py +++ b/skills/x-twitter/x-post.py @@ -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 diff --git a/src/agent-api.py b/src/agent-api.py index a24fab2a4..dab46e030 100644 --- a/src/agent-api.py +++ b/src/agent-api.py @@ -50,11 +50,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 diff --git a/tests/agent-api-twilio-auth.test.py b/tests/agent-api-twilio-auth.test.py new file mode 100644 index 000000000..f90c76229 --- /dev/null +++ b/tests/agent-api-twilio-auth.test.py @@ -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) From c336fc8113386fb4d4e300751ef051e52ea7217b Mon Sep 17 00:00:00 2001 From: john-the-dev <52230987+john-the-dev@users.noreply.github.com> Date: Tue, 7 Jul 2026 04:21:19 -0700 Subject: [PATCH 2/2] docs(x-twitter): add pip install step to SKILL.md Setup section x-post.py no longer auto-installs requests/requests-oauthlib (removed in the previous commit to eliminate the supply-chain risk from unpinned pip installs). Document the one-time install step in SKILL.md so operators know what to run before first use. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01QB3qZQrwponfum2JFNvHeh --- skills/x-twitter/SKILL.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/skills/x-twitter/SKILL.md b/skills/x-twitter/SKILL.md index ebf7f3f65..c3d373265 100644 --- a/skills/x-twitter/SKILL.md +++ b/skills/x-twitter/SKILL.md @@ -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=...