Blog Posts

{ }

Writing Arbitrary Files with the Simple Voice Chat mod

How a trusted username cache and an unchecked audio channel let me create files anywhere on your computer.

minecraftsecurityvulnerabilityvoice-chatpath-traversal

Writing Arbitrary Files with the Simple Voice Chat mod

Voice chat is one of the most popular mods in the Minecraft ecosystem. The idea of having spatial audio communication right in the game is appealing, and a lot of servers run it. Seemed like an interesting target to audit.

So about literally 40 minutes of digging thru the source code, I found a path traversal in Simple Voice Chat that lets a malicious server write new .mp3 files to arbitrary locations while you are recording. The bug is not RCE because existing files cannot be overwritten and the content is always valid MP3 audio, but it is still a clean arbitrary file creation primitive. And there are some pretty funny things you can do with this.

How It Works

The attack chain has five pieces, all living in different parts of the codebase.

Step 1: Trust Without Validation

First, ClientPlayerStateManager receives a PlayerState packet from the server and immediately calls USERNAME_CACHE.updateUsernameAndSave(uuid, name) with zero checks. The server controls both the UUID and the name string. There is no verification that the UUID belongs to a real player, and the name parameter is never sanitized.

ClientPlayerStateManager trusting server PlayerState

Step 2: Persistence to Disk

UsernameCache stores this mapping in a Map and flushes it to disk as JSON. Once saved, it survives client restarts. This is the dangerous part: you can send a malicious username with path traversal characters like ../../../config/file, and the client will cache and persist it forever.

Step 3: The Vulnerable Path Resolution

Later, when recording is active, AudioRecorder.lookupName(uuid) returns that cached name verbatim with no sanitization. If the cached username is something like ../../../config/important, those ../ sequences stay in the path untouched.

AudioRecorder lookupName returning unsanitized username

The recorder then creates a new encoder and opens an output stream at that traversed path:

AudioRecorder creating the output stream at the traversed path

The directory tree is created eagerly via mkdirs() in the constructor, so the traversed directories exist before any audio arrives. The file is created at that traversed location with CREATE_NEW, which fails only if the file already exists.

Step 4: Unverified Packet Routing

Next is the trigger. AudioChannel.writeToSpeaker has two branches. The PlayerSoundPacket branch verifies that the UUID corresponds to an actual player entity. The GroupSoundPacket branch does not. It just calls appendChunk with whatever UUID was in the packet.

AudioChannel GroupSoundPacket branch skipping entity verification

A plugin can use createStaticAudioChannel to send spoofed group audio for any UUID, including one whose cached username contains a traversal payload.

Step 5: User-Initiated Recording

Finally, a recorder only exists if the victim manually started recording through the GUI or keybind. Both are local user actions, and the keybind is unbound by default. The server cannot trigger recording remotely. It can only stream audio once the victim has already started recording.

Putting It Together

So the attack looks like this: First, the plugin sends a PlayerState packet with a traversal payload as the username. Something like ../../../some/path. The client caches this UUID to name mapping and saves it to disk. Then the victim starts recording through the GUI. The plugin sends GroupSoundPacket messages using that same UUID. The recorder looks up the username in the cache, gets the traversal path, and opens an OutputStream to that location with CREATE_NEW. Audio data streams into that file. LAME encodes it as valid MP3 at 320 kbps. When done, you have a new .mp3 file at an arbitrary location on the victim's disk.

The only constraints are that the extension is always .mp3, you cannot overwrite existing files (CREATE_NEW fails on existing files), and the content is always valid MP3 audio. You cannot inject arbitrary bytes into the file. So RCE via JAR polyglot is not possible.

How I Had Fun With This

Here is the funny part. Once you have arbitrary file creation, your options are pretty wide. If the player is on Windows, you could traverse to something like ../../../Microsoft/Windows/Start Menu/Programs/Startup and dump an MP3 file there. Every time they restart their computer, that audio file will play, any duration, any sound. You could also just flag their desktop with hundreds of MP3 files, or fill their storage with gigabyte sized files to make them run out of disk space. Pretty funny way to troll someone, and all from a Minecraft voice chat plugin. (This was tested on my friends, with their consent, of course.)

The creativity here is in the path. You could target config files, user documents, or any directory you know exists. The limitation is that you can only create new files, not overwrite existing ones, and the content has to be valid MP3. But within those bounds, the attack surface is actually pretty large.

What Makes This Interesting

What makes this interesting is how spread out the vulnerability is. ClientPlayerStateManager trusts the input. UsernameCache persists it. AudioRecorder uses it as a filesystem path. AudioChannel provides an unverified entry point for packet routing. None of these components are dangerous alone, but together they form a complete exploitation chain. I spent most of my time ruling out RCE vectors before accepting that this is strictly a file creation bug, albeit one with full path control.

The lesson here is that security boundaries matter. Even in trusted connections like a Minecraft server you are connected to, blindly trusting input at every layer creates unexpected vulnerabilities. Input validation should happen early and often, and dangerous APIs like constructing filesystem paths from user-controlled data should be wrapped with sanitization.

This vulnerability was responsibly disclosed to the mod developer, and it has been fixed in the latest version. Players should update to that version to avoid being exploited by malicious servers.