fix(security): enforce VLLM_MAX_AUDIO_CLIP_FILESIZE_MB on all audio paths (#53561)
Signed-off-by: jperezde <jperezde@redhat.com>
Juan Pérez de Algaba committed
Aug 24, 2026 at 23:37 UTC
0d7d5ed0b2b61da53f682534f1754fe7d0251a34
4 files changed
+27
-5
docs/usage/security.md
+1
-1
@@ -95,7 +95,7 @@ denial of service:
95
| --- | --- | --- |
96
| `VLLM_MAX_MEDIA_DOWNLOAD_SIZE_MB` | `256` | Maximum size in MB for a single remote media response. Oversized responses are rejected while streaming before the full body is materialized in memory. |
97
| `VLLM_MAX_IMAGE_PIXELS` | `178956970` (~179M pixels) | Maximum decoded image size in pixels. Images exceeding this are rejected before raster memory is allocated. Default matches PIL's built-in 2x decompression-bomb threshold (~680 MB for RGB). |
98
-| `VLLM_MAX_AUDIO_CLIP_FILESIZE_MB` | `25` | Maximum filesize in MB for a single audio file. |
98
+| `VLLM_MAX_AUDIO_CLIP_FILESIZE_MB` | `25` | Maximum compressed filesize in MB for a single audio file. Enforced on all audio inputs (multimodal chat URLs, speech-to-text uploads, data: URLs, and local file paths) before decoding begins. |
99
| `VLLM_MAX_AUDIO_DECODE_DURATION_S` | `600` | Maximum decoded audio duration in seconds. Prevents compressed audio from expanding into gigabytes of float32 PCM. |
100
| `VLLM_MAX_AUDIO_DECODE_BYTES` | `268435456` (256 MiB) | Maximum float32 PCM bytes that audio decoding may allocate. Guards against sample-rate forgery where an inflated header sample rate bypasses the duration guard while the actual frame count causes a multi-GiB allocation. |
101
tests/test_audio_media_size_precheck.py
+21
-1
@@ -2,6 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
from collections.abc import AsyncIterator, Iterator
5
+from pathlib import Path
6
from unittest.mock import patch
7
8
import pytest
@@ -18,7 +19,7 @@ pytestmark = [pytest.mark.cpu_test, pytest.mark.skip_global_cleanup]
19
class _SyncResponse:
20
def __init__(self, chunks: list[bytes], content_length: int | None = None):
21
self.headers = (
21
- {} if content_length is None else {"content-length": str(content_length)}
22
+ {} if content_length is None else {"Content-Length": str(content_length)}
23
)
24
self._chunks = chunks
25
self.iterated = 0
@@ -79,6 +80,25 @@ def test_audio_base64_rejects_before_decode(monkeypatch: pytest.MonkeyPatch):
80
decode.assert_not_called()
81
82
83
+def test_audio_load_bytes_rejects_oversized(monkeypatch: pytest.MonkeyPatch):
84
+ monkeypatch.setattr(envs, "VLLM_MAX_AUDIO_CLIP_FILESIZE_MB", 1)
85
+
86
+ with pytest.raises(VLLMValidationError, match="Maximum file size exceeded"):
87
+ AudioMediaIO().load_bytes(b"\x00" * (MiB_bytes + 1))
88
+
89
+
90
+def test_audio_load_file_rejects_oversized(
91
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
92
+):
93
+ monkeypatch.setattr(envs, "VLLM_MAX_AUDIO_CLIP_FILESIZE_MB", 1)
94
+
95
+ oversized = tmp_path / "big.wav"
96
+ oversized.write_bytes(b"\x00" * (MiB_bytes + 1))
97
+
98
+ with pytest.raises(VLLMValidationError, match="Maximum file size exceeded"):
99
+ AudioMediaIO().load_file(oversized)
100
+
101
+
102
def test_sync_http_reader_rejects_from_content_length_before_body_read():
103
response = _SyncResponse([b"A" * 10], content_length=MiB_bytes + 1)
104
connection = HTTPConnection()
vllm/envs.py
+4
-3
@@ -987,9 +987,10 @@ environment_variables: dict[str, Callable[[], Any]] = {
987
"VLLM_MEDIA_LOADING_THREAD_COUNT": lambda: int(
988
os.getenv("VLLM_MEDIA_LOADING_THREAD_COUNT", "8")
989
),
990
- # Maximum filesize in MB for a single audio file when processing
991
- # speech-to-text requests. Files larger than this will be rejected.
992
- # Default is 25 MB
990
+ # Maximum filesize in MB for a single audio file. Enforced on all
991
+ # audio inputs (multimodal chat, speech-to-text uploads, data: URLs,
992
+ # and local file:// paths). Files larger than this will be rejected
993
+ # before decoding. Default is 25 MB.
994
"VLLM_MAX_AUDIO_CLIP_FILESIZE_MB": lambda: int(
995
os.getenv("VLLM_MAX_AUDIO_CLIP_FILESIZE_MB", "25")
996
),
vllm/multimodal/media/audio.py
+1
@@ -312,6 +312,7 @@ class AudioMediaIO(MediaIO[tuple[npt.NDArray, float]]):
312
return self.load_bytes(pybase64.b64decode(data))
313
314
def load_file(self, filepath: Path) -> tuple[npt.NDArray, float]:
315
+ self._validate_encoded_size(filepath.stat().st_size)
316
return load_audio(
317
filepath,
318
sr=None,