
Reachy Mini’s Unauthenticated Upload Endpoint Accepts More Than Sounds
A Reachy Mini daemon endpoint accepts unauthenticated uploads without extension, content, or size validation. Here’s why a seemingly minor media API flaw matters as part of a broader compromise chain.
A sound upload endpoint with no meaningful upload controls
A vulnerability advisory for reachy_mini was published yesterday: the daemon’s /api/media/sounds/upload endpoint accepts file uploads without authentication or file validation.
The endpoint is supposed to store sound files. In practice, it accepts arbitrary file content and writes it to the daemon’s filesystem under /tmp/reachy_mini_sounds/<original_filename>.
That distinction matters. An endpoint that is designed for .wav files but accepts shell scripts, configuration files, or other unexpected content is not merely being permissive. It is creating a file-placement primitive for anyone who can reach it.
And according to the advisory, that endpoint is reachable by unauthenticated users.
The bug is not that a sound file can be uploaded. The bug is that the server never proves the upload is a sound file, and never proves the caller is allowed to upload anything.
What the handler actually does
The vulnerable handler is located in src/daemon/app/routers/media.py, in the upload_sound method. Its behavior is straightforward:
- Require a filename.
- Strip directory components from that filename.
- Create the temporary sounds directory.
- Read the entire uploaded file.
- Write its bytes to disk.
- Return the absolute path.
The relevant logic looks like this:
@router.post("/sounds/upload")
async def upload_sound(
file: UploadFile = File(...),
) -> dict[str, str]:
if not file.filename:
raise HTTPException(status_code=400, detail="Filename is required")
filename = Path(file.filename).name
if not filename or filename in (".", ".."):
raise HTTPException(status_code=400, detail="Invalid filename")
os.makedirs(SOUNDS_TMP_DIR, exist_ok=True)
dest = os.path.join(SOUNDS_TMP_DIR, filename)
content = await file.read()
with open(dest, "wb") as f:
f.write(content)
return {"status": "ok", "path": dest}There is one useful defense here: Path(file.filename).name removes path components, and the handler rejects . and ... That addresses a narrow path traversal concern.
But it does not make the upload safe.
The handler has no authentication check. It does not restrict file extensions. It does not inspect file contents. It does not enforce an upload size. It overwrites an existing file with the same name. The endpoint returns the absolute path of the saved file as well.
In other words, the implementation protects the shape of the filename while leaving the identity and contents of the file almost entirely uncontrolled.
The proof of concept is deliberately simple
The advisory demonstrates the issue by first uploading a normal WAV file:
curl -X POST http://<daemon_domain>:<daemon_port>/api/media/sounds/upload \
-F "file=@/path/to/your/file.wav"Then it uploads a shell script through the same endpoint:
curl -X POST http://<daemon_domain>:<daemon_port>/api/media/sounds/upload \
-F "file=@/path/to/your/script.sh"The important result is not whether the script executes immediately. The result is that the server accepts the file and writes it to disk even though the endpoint is intended for sound files.
That is the kind of behavior that gets underestimated during review. People see a temporary directory and think, “It’s only media.” But a temporary directory is still part of the operating system. Files placed there can be read by other components, picked up by later workflows, loaded by unsafe code, or used as payload storage when another vulnerability becomes available.
The advisory describes this as allowing an attacker to harm the integrity of stored data and propagate a foothold if additional vulnerabilities arise. That is the right way to frame it. An unrestricted upload is not automatically remote code execution. It is an attacker-controlled file write. Its eventual severity depends on what else consumes those files and what other attack paths exist.
Exposure is broader than the route itself
The daemon is bound to 0.0.0.0 by default, meaning it listens on all network interfaces. The application also uses permissive CORS with allow_origins=["*"].
Those settings do not independently turn the upload bug into code execution. They do, however, widen the set of clients that may be able to interact with the daemon and make the absence of endpoint authentication more consequential.
A local-only service with a carefully controlled access path is one risk profile. An unauthenticated service listening across every connected interface is another.
This is a recurring API security failure: the developer thinks in terms of the intended client, while the network sees an HTTP endpoint. If the route is exposed and does not authenticate callers, anyone who can reach it gets to define the request.
CORS deserves a precise explanation here. Browser cross-origin policy is not an authentication mechanism, and permissive CORS is not required for every non-browser client to call an API. A command-line client, another local process, or a service on the same reachable network does not need a browser’s permission model. The core issue is still the missing authentication check. The wildcard CORS policy simply removes another browser-side boundary for web clients.
This is part of a larger compromise chain
The advisory places the unrestricted upload issue in a chain described as allowing an unauthenticated user to gain root access on the Reachy’s operating system:
- Unrestricted file upload in the media sounds API
- Bluetooth authentication bypass
- Bluetooth directory traversal
The upload issue is therefore being reported as one link in a broader sequence, not as a standalone claim that uploading a .sh file instantly produces root access.
That distinction is important for anyone triaging the finding. The immediate primitive is arbitrary file upload to the daemon’s temporary sounds directory. The larger impact comes from combining it with the other weaknesses identified in the chain.
Security reviews often fail at exactly this boundary. A team evaluates each endpoint in isolation and concludes that a media upload is low risk because the application does not execute media files. An attacker evaluates reachable primitives together: unauthenticated access, file placement, directory traversal, local services, privileged processes, and trust relationships between components.
A file upload does not need to be an executable upload to matter. It only needs to land somewhere that a later step can use.
What a real fix needs to cover
The advisory recommends three categories of remediation:
- Allow-list the file extensions the endpoint is supposed to accept.
- Validate that the content matches the claimed file type using magic numbers and known file structure.
- Enforce authentication on the upload endpoint.
Those are the minimum controls. They should be implemented as separate checks, because each one addresses a different failure mode.
1. Authenticate before reading the file
The server should establish who is calling the endpoint and whether that caller has permission to upload media. Authentication belongs at the route boundary, before the application reads and stores attacker-controlled content.
Do not treat the presence of a valid multipart request as proof of authorization. It only proves that the request has the expected HTTP shape.
If the daemon is intended to be used only locally, that still needs to be enforced deliberately through authentication and network controls. Binding to all interfaces while assuming that the device’s network is trusted is a fragile default.
2. Use an allow-list, not a deny-list
If the endpoint only supports a small number of sound formats, accept only those formats. Rejecting known-bad suffixes such as .sh is weaker than accepting only known-good suffixes.
A deny-list eventually becomes a game of catching up with every unexpected extension. An allow-list makes the intended contract explicit.
The filename should also be treated as untrusted metadata. Strip path components, as the current code does, but do not let a user-controlled filename decide more than necessary. A server-generated storage name is generally safer than preserving arbitrary names, particularly when duplicate names overwrite existing files.
3. Inspect the content, not just the name
An attacker can call a file sound.wav without changing its contents. Extension validation alone does not establish that the bytes are a valid sound file.
The server should check the file’s signature and expected structure for every supported type. If the application uses a library to parse the file, parsing should happen before the file is made available to other components. Invalid or malformed content should be rejected rather than stored for later interpretation.
This is also where resource limits matter. The advisory specifically identifies the lack of file size validation. Reading the entire upload with await file.read() means the handler has no visible bound on how much content it will load before writing it. A production upload path should enforce a maximum size and avoid allowing an unbounded request to consume memory or disk space.
4. Do not overwrite by default
The current behavior overwrites a file when another upload uses the same name. That creates an unnecessary integrity risk and makes collisions part of the API’s behavior.
A safer design is to generate a unique server-side name, store the file in a controlled directory, and keep the original filename only as metadata if the application needs it. If replacement is a legitimate operation, it should be explicit and authorized rather than an automatic side effect of choosing a filename.
5. Keep the returned data minimal
Returning the absolute filesystem path is not needed for most upload APIs. It exposes internal layout and couples the client to the daemon’s filesystem.
A returned media identifier or application-relative resource reference is a cleaner interface. It does not solve the upload vulnerability by itself, but it reduces unnecessary disclosure and makes it easier to change storage later.
What operators should do now
If you operate Reachy Mini daemons, treat the upload endpoint as exposed until you have verified otherwise.
Start with the practical checks:
- Determine whether the daemon is running with the media API enabled.
- Check whether it is bound to
0.0.0.0or otherwise reachable from networks that should not access it. - Review whether the upload route is protected by authentication in your deployed version.
- Inspect
/tmp/reachy_mini_soundsfor unexpected file types and suspicious filenames. - Look for repeated uploads, overwrites, and requests to
/api/media/sounds/uploadin whatever logs are available. - Restrict network access to the daemon while remediation is in progress.
- Review the other links in the reported compromise chain rather than treating the upload issue as an isolated defect.
Do not rely on the filename extension of existing files as proof that they are safe. The reported behavior is specifically that arbitrary content can be uploaded under an allowed-looking or unexpected name. Inspect what is actually on disk.
The advisory references a fix associated with pollen-robotics/reachy_mini@984c772, but the source material does not provide a release version or a complete patch description. Before making deployment decisions, compare your installed code against the project’s fix and verify which version contains the remediation.
The broader lesson for API reviews
Media endpoints are often treated as low-risk because the content is not supposed to execute. That is a mistake.
The right questions are more basic:
- Who can call this route?
- What does the server write, and where?
- Can the caller control the filename or storage path?
- Can the caller overwrite existing data?
- Does the server validate the bytes or only the metadata?
- What process will read the stored file next?
- Is the service reachable beyond the device or process that needs it?
These questions apply to profile images, document uploads, firmware packages, logs, audio files, and every other “just upload it” feature.
The Reachy Mini issue is a clean example because the handler is easy to understand. It does one useful thing—removes path components—but leaves the high-value controls absent. A basename check is not an upload security policy. It is one input-sanitization step inside a larger policy that still needs authentication, type validation, size limits, safe storage, and careful downstream handling.
An upload endpoint should enforce the contract its name implies. If it is a sound upload API, it should accept authenticated requests containing valid, bounded sound files—and nothing else. Anything weaker turns a convenience route into an attacker-controlled write primitive.
Related posts
- Security
How I got free cinema credit by ordering -2 popcorns
A missing input validation on M-Tix Cinema XXI's food ordering API let me increase my account balance by submitting negative quantities. No tools needed — just a browser.
May 19, 2026 · 6 min - Security
How I analyze API security headers in 30 seconds
A quick checklist for reading HTTP response headers and spotting security misconfigurations before you even look at the response body.
May 18, 2026 · 7 min - Security
Common auth mistakes I find when reverse-engineering APIs
After years of poking at APIs that weren't meant to be poked at, these are the auth patterns that break most often — and why.
May 18, 2026 · 9 min