| Title | https://github.com/EricLBuehler/ mistral.rs v0.9.2 Unauthenticated arbitrary-path file write |
|---|
| Description | Unauthenticated arbitrary-path file write via /calibration/apply save_cimatrix
## Summary
The `/calibration/apply` HTTP endpoint accepts an optional `save_cimatrix`
string field and writes the server's collected calibration statistics to
that path with `fs::write`. The only validation is that the path's
extension equals `cimatrix`; there is no check that the path stays within
any server-controlled directory. An unauthenticated client (this endpoint,
like the rest of the default HTTP server, requires no authentication)
supplies an absolute path or a relative path containing `..` and the
server writes to it.
## Details
`calibration_apply` (`mistralrs-server-core/src/handlers.rs`) takes the
request body's `save_cimatrix: Option<String>` field, converts it directly
to a `PathBuf` with no validation, and dispatches it into the pipeline:
```rust
pub async fn calibration_apply(
State(state): ExtractedMistralRsState,
payload: Result<Json<CalibrationApplyRequest>, JsonRejection>,
) -> Response {
let request = match payload {
Ok(Json(request)) => request,
Err(error) => return openai_error_response(ApiError::from_json_rejection(error)),
};
send_calibration(
&state,
mistralrs_core::CalibrationAction::Apply {
save_cimatrix: request.save_cimatrix.map(std::path::PathBuf::from),
},
)
.await
}
```
The request reaches `apply_calibration` in
`mistralrs-core/src/pipeline/isq_flow/online.rs`, whose only check on the
path was:
```rust
if let Some(path) = save_cimatrix {
if path.extension().is_none_or(|ext| ext != "cimatrix") {
anyhow::bail!(
"save_cimatrix path `{}` must end in .cimatrix",
path.display()
);
}
}
```
which then reaches `CollectedImatrixData::save_imatrix`
(`mistralrs-quant/src/imatrix.rs`):
```rust
pub fn save_imatrix<P: AsRef<Path>>(&self, fname: P) -> Result<()> {
if let Some(ext) = fname.as_ref().extension() {
if ext != "cimatrix" {
candle_core::bail!(...);
}
}
let mut buf: Vec<u8> = Vec::new();
// ... encode collected imatrix data ...
fs::write(fname, buf)?;
Ok(())
}
```
Neither check inspects the path's directory component. `path.is_absolute()`
is never checked, and `..` components are never rejected, so
`/etc/cron.d/x.cimatrix`, `/home/user/.ssh/authorized_keys.cimatrix`, or
`../../../../some/other/path.cimatrix` all pass the extension check and
reach `fs::write` with no directory restriction. The endpoint is
reachable with no authentication, matching the threat model already
established by
GHSA-wfgq-w7cq-qj7j and GHSA-m3wp-48jr-vr4g for this server (unauthenticated
by default).
The write's content is the server's own collected activation statistics
(binary, little-endian encoded key/value pairs), not attacker-supplied
bytes, so this is a write-primitive with a semi-fixed payload rather than
arbitrary content injection. The exploitable primitive is the destination:
overwriting an existing file the server process can write to (data
corruption, denial of service against another service or the model's own
files), or planting a file at an attacker-chosen path for pickup by
whatever else consumes that location.
## Proof of Concept
The affected logic (extension check plus `fs::write`) is self-contained
and reproduced verbatim below with `anyhow`/`String` errors in place of
`candle_core::Result`, since the vulnerable step never touches tensors.
`Cargo.toml`:
```toml
[package]
name = "cimatrix_write_harness"
version = "0.0.0"
edition = "2021"
[dependencies]
tempfile = "3"
```
`src/lib.rs`:
```rust
use std::path::{Path, PathBuf};
fn online_rs_extension_check(path: &Path) -> Result<(), String> {
if path.extension().is_none_or(|ext| ext != "cimatrix") {
return Err(format!("save_cimatrix path `{}` must end in .cimatrix", path.display()));
}
Ok(())
}
fn save_imatrix_vuln(fname: &Path, payload: &[u8]) -> Result<(), String> {
if let Some(ext) = fname.extension() {
if ext != "cimatrix" {
return Err(format!("Expected a .cimatrix file, got {ext:?}"));
}
}
std::fs::write(fname, payload).map_err(|e| e.to_string())
}
pub fn handle_save_cimatrix_vuln(requested_path: &str, payload: &[u8]) -> Result<(), String> {
let path = PathBuf::from(requested_path);
online_rs_extension_check(&path)?;
save_imatrix_vuln(&path, payload)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shipped_chain_allows_arbitrary_absolute_path() {
let dir = tempfile::tempdir().unwrap();
let outside_target = dir.path().join("outside_root.cimatrix");
assert!(!outside_target.exists());
let result = handle_save_cimatrix_vuln(outside_target.to_str().unwrap(), b"data");
assert!(result.is_ok());
assert!(outside_target.exists());
}
#[test]
fn shipped_chain_allows_dot_dot_traversal() {
let allowed = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let traversal = format!(
"{}/../{}/escaped.cimatrix",
allowed.path().display(),
outside.path().file_name().unwrap().to_str().unwrap()
);
let result = handle_save_cimatrix_vuln(&traversal, b"x");
assert!(result.is_ok());
assert!(outside.path().join("escaped.cimatrix").exists());
}
}
```
Result: both tests pass, confirming the shipped chain writes to an
arbitrary absolute path and follows `..` traversal, with only the
`.cimatrix` extension enforced.
End-to-end request shape against a default deployment (no auth
configured):
```
POST /calibration/apply
Content-Type: application/json
{"save_cimatrix": "/tmp/attacker_controlled_path.cimatrix"}
```
(Calibration must have been started via `POST /calibration/start` first so
`apply_calibration` has data to harvest; the path check runs before that
requirement changes the outcome for the path itself.)
## Impact
An unauthenticated client of a default mistral.rs deployment can direct the
server process to write a file at any absolute path, or any path reached
via `..` traversal, that the process has write permission for. Because the
write is unconditional (`fs::write` truncates and overwrites), this can
corrupt or destroy an existing file at that location (integrity and
availability impact), or place a new file where some other process or
configuration mechanism might later read it. The written content is the
server's internal binary imatrix encoding, not attacker-chosen bytes, which
limits (but does not eliminate) the impact compared to full content
control.
## Fix
Reject any `save_cimatrix` path that is absolute or contains a `..`
component, restricting writes to a relative location under the server's
working directory:
```rust
fn validate_save_cimatrix_path(path: &std::path::Path) -> anyhow::Result<()> {
if path.extension().is_none_or(|ext| ext != "cimatrix") {
anyhow::bail!("save_cimatrix path `{}` must end in .cimatrix", path.display());
}
if path.is_absolute() || path.components().any(|c| c == std::path::Component::ParentDir) {
anyhow::bail!(
"save_cimatrix path `{}` must be a relative path within the working directory (no absolute paths or `..`)",
path.display()
);
}
Ok(())
}
```
called from `apply_calibration` in place of the previous extension-only
check, before any calibration data is harvested.
## CVSS
CVSS 3.1: 5.3 (Medium) - AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L.
Network-reachable, no privileges or user interaction required, low
complexity. Integrity and availability impact are limited to Low because
the write content is not attacker-controlled (fixed binary encoding of
server-side calibration state) and the primitive is overwrite/plant rather
than arbitrary content injection; confidentiality is unaffected.
|
|---|
| Source | ⚠️ https://github.com/EricLBuehler/mistral.rs/commit/8c8105671 |
|---|
| User | carfeii (UID 97470) |
|---|
| Submission | 09/24/2026 12:47 (1 month ago) |
|---|
| Moderation | 10/11/2026 20:33 (17 days later) |
|---|
| Status | Accepted |
|---|
| VulDB entry | 416764 [EricLBuehler Mistral.rs up to 0.9.3 handlers.rs calibration_apply save_cimatrix path traversal] |
|---|
| Points | 20 |
|---|