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,