Hugging Face LeRobot CVE-2026-25874: Unauthenticated Pickle RCE Over gRPC, Still Unpatched
Introduction
A critical pre-authentication remote code execution vulnerability has been disclosed in LeRobot, Hugging Face's open-source robotics framework with nearly 24,000 GitHub stars. The flaw — CVE-2026-25874 (CVSS 9.8) — sits in LeRobot's async inference pipeline, where the gRPC PolicyServer and robot client both call pickle.loads() on attacker-controlled bytes received over an unauthenticated, plaintext gRPC channel. There is no patch yet: a fix is planned for version 0.6.0 but the live main branch and every release through 0.5.1 are exploitable, and a public proof-of-concept already exists.
What Happened
LeRobot's async inference module sets up a gRPC server using grpc.add_insecure_port() — no TLS, no token verification, no auth interceptor. Three RPC methods on that server (SendPolicyInstructions, SendObservations, and GetActions) carry a raw bytes data field that the server then deserialises with pickle.loads(). Worse, the validation happens after deserialisation: the code only checks isinstance(policy_specs, RemotePolicyConfig) once pickle.loads() has already executed any payload the attacker shipped in.
The original developers were aware of the risk — both call sites carry a # nosec comment that suppresses the Bandit static analyser's deserialisation warning — but no actual mitigation was implemented. VulnCheck researcher Valentin Lobstein confirmed exploitation against LeRobot 0.4.3, and a separate researcher named "chenpinji" had already reported the same issue back in December 2025; the LeRobot team responded in January acknowledging that "that part of the codebase needs to be almost entirely refactored."
The PoC is roughly fifteen lines of Python: open an insecure channel to the PolicyServer, build a class whose __reduce__ returns (os.system, ("id > /tmp/lerobot_pwned",)), send the pickled payload as PolicySetup.data, and watch the server execute the command as the LeRobot service user. The gRPC call returns StatusCode.UNKNOWN because the type check fails on the resulting int, but the RCE has already fired before that branch is reached.
Why It Matters
LeRobot is not a toy library. It backs Hugging Face's "open-source robotics" push and is being adopted as the inference plane behind real hardware — research robots, mobile manipulators, teleop rigs — that share networks with cameras, datasets, and pre-trained model artefacts. AI inference processes typically run with elevated privileges so they can reach GPUs, model storage, and internal APIs; "unauthenticated RCE in the inference server" therefore translates very quickly into model theft, dataset exfiltration, lateral movement, and in the robotic case, physical safety risk if the same host can issue motion commands.
There is also irony to call out for any team building ML platforms: Hugging Face itself created safetensors precisely because pickle is unsafe for ML data, then shipped a robotics framework that pickles attacker-controlled network input with # nosec comments next to the calls. If you have any internal services that use pickle.loads() on anything that crosses a network boundary, this disclosure is a very loud reminder to audit them today rather than the day a CVE lands in your name.
Who Is Affected
- All LeRobot versions through 0.5.1 (and
mainuntil at least the 0.6.0 release). - Any organisation running the LeRobot async inference PolicyServer, including research labs, teleoperation rigs, and production robotics deployments.
- Robot clients that connect to a PolicyServer — the same
pickle.loads()pattern in the client means a malicious or compromised server can RCE the connected robot. - By extension, anyone whose ML platform relies on pickle for cross-process or cross-network serialisation while implicitly trusting the producer.
How to Protect Yourself
Until a patched 0.6.0 is shipped, treat every LeRobot PolicyServer port as a privileged shell waiting for the wrong client. Hard-block it at the network layer.
Find exposed instances on your own network first:
# default LeRobot async inference port is 50051; sweep for it
nmap -p 50051 -sV --script=grpc-list-services 10.0.0.0/16
# look for AsyncInference among the services
For Linux hosts running LeRobot, restrict the gRPC port to a known operator subnet:
# example with iptables — replace 10.10.20.0/24 with your trusted control plane
iptables -A INPUT -p tcp --dport 50051 -s 10.10.20.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 50051 -j DROP
# or with nftables
nft add rule inet filter input tcp dport 50051 ip saddr != 10.10.20.0/24 drop
If you can fork or patch your own deployment, do not wait for upstream — the unsafe call sites are well documented and easy to wrap. At minimum, refuse to deserialise unless the channel is authenticated:
# replace add_insecure_port() with mTLS server credentials
import grpc
with open("/etc/lerobot/server.key", "rb") as f: key = f.read()
with open("/etc/lerobot/server.crt", "rb") as f: crt = f.read()
with open("/etc/lerobot/ca.crt", "rb") as f: ca = f.read()
creds = grpc.ssl_server_credentials(
[(key, crt)], root_certificates=ca, require_client_auth=True
)
server.add_secure_port("0.0.0.0:50051", creds)
For the actual deserialisation, drop pickle in favour of safetensors plus JSON for the small amount of metadata the protobuf carries:
# inside SendPolicyInstructions / SendObservations
import json
from safetensors.numpy import load as st_load
policy_meta = json.loads(request.metadata_json) # strings, ints, dicts only
tensors = st_load(request.tensor_blob) # safe, no exec
For threat hunting, look for the gRPC call patterns the public PoC uses. Pickle payloads are easy to fingerprint on the wire — they start with a small header and almost always contain __reduce__ or os strings when used for command execution:
# capture and grep gRPC traffic on the LeRobot port
tcpdump -i any -nn -A -s0 'tcp port 50051' -w /tmp/lerobot.pcap
strings /tmp/lerobot.pcap | grep -E '(__reduce__|os\.system|subprocess|nt\\.CommandShell|cposix)'
# host-level: look for unexpected child processes spawned by the LeRobot service
ps -eo pid,ppid,user,comm,args --forest | grep -i lerobot
auditctl -w /usr/bin/sh -p x -k lerobot-shell
ausearch -k lerobot-shell -i | tail
If a PolicyServer was internet-reachable or exposed to a shared LAN at any point, treat the host as compromised: rotate any credentials cached on it (HuggingFace tokens, dataset bucket keys, SSH keys), revoke connected robot client certificates, and reimage before re-deploying with TLS plus mutual auth in place.