From 0d7d5ed0b2b61da53f682534f1754fe7d0251a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20P=C3=A9rez=20de=20Algaba?= <124347725+jperezdealgaba@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:37:02 +0200 Subject: [PATCH] fix(security): enforce VLLM_MAX_AUDIO_CLIP_FILESIZE_MB on all audio paths (#53561) Signed-off-by: jperezde --- docs/usage/security.md | 2 +- tests/test_audio_media_size_precheck.py | 22 +++++++++++++++++++++- vllm/envs.py | 7 ++++--- vllm/multimodal/media/audio.py | 1 + 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/usage/security.md b/docs/usage/security.md index 8235d946c3..4631d7ce52 100644 --- a/docs/usage/security.md +++ b/docs/usage/security.md @@ -95,7 +95,7 @@ denial of service: | --- | --- | --- | | `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. | | `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). | -| `VLLM_MAX_AUDIO_CLIP_FILESIZE_MB` | `25` | Maximum filesize in MB for a single audio file. | +| `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. | | `VLLM_MAX_AUDIO_DECODE_DURATION_S` | `600` | Maximum decoded audio duration in seconds. Prevents compressed audio from expanding into gigabytes of float32 PCM. | | `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. | diff --git a/tests/test_audio_media_size_precheck.py b/tests/test_audio_media_size_precheck.py index f9a4d772e8..e1f2ed8df4 100644 --- a/tests/test_audio_media_size_precheck.py +++ b/tests/test_audio_media_size_precheck.py @@ -2,6 +2,7 @@ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project from collections.abc import AsyncIterator, Iterator +from pathlib import Path from unittest.mock import patch import pytest @@ -18,7 +19,7 @@ pytestmark = [pytest.mark.cpu_test, pytest.mark.skip_global_cleanup] class _SyncResponse: def __init__(self, chunks: list[bytes], content_length: int | None = None): self.headers = ( - {} if content_length is None else {"content-length": str(content_length)} + {} if content_length is None else {"Content-Length": str(content_length)} ) self._chunks = chunks self.iterated = 0 @@ -79,6 +80,25 @@ def test_audio_base64_rejects_before_decode(monkeypatch: pytest.MonkeyPatch): decode.assert_not_called() +def test_audio_load_bytes_rejects_oversized(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(envs, "VLLM_MAX_AUDIO_CLIP_FILESIZE_MB", 1) + + with pytest.raises(VLLMValidationError, match="Maximum file size exceeded"): + AudioMediaIO().load_bytes(b"\x00" * (MiB_bytes + 1)) + + +def test_audio_load_file_rejects_oversized( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +): + monkeypatch.setattr(envs, "VLLM_MAX_AUDIO_CLIP_FILESIZE_MB", 1) + + oversized = tmp_path / "big.wav" + oversized.write_bytes(b"\x00" * (MiB_bytes + 1)) + + with pytest.raises(VLLMValidationError, match="Maximum file size exceeded"): + AudioMediaIO().load_file(oversized) + + def test_sync_http_reader_rejects_from_content_length_before_body_read(): response = _SyncResponse([b"A" * 10], content_length=MiB_bytes + 1) connection = HTTPConnection() diff --git a/vllm/envs.py b/vllm/envs.py index 42643b32ee..a24debdebd 100755 --- a/vllm/envs.py +++ b/vllm/envs.py @@ -987,9 +987,10 @@ environment_variables: dict[str, Callable[[], Any]] = { "VLLM_MEDIA_LOADING_THREAD_COUNT": lambda: int( os.getenv("VLLM_MEDIA_LOADING_THREAD_COUNT", "8") ), - # Maximum filesize in MB for a single audio file when processing - # speech-to-text requests. Files larger than this will be rejected. - # Default is 25 MB + # Maximum filesize in MB for a single audio file. Enforced on all + # audio inputs (multimodal chat, speech-to-text uploads, data: URLs, + # and local file:// paths). Files larger than this will be rejected + # before decoding. Default is 25 MB. "VLLM_MAX_AUDIO_CLIP_FILESIZE_MB": lambda: int( os.getenv("VLLM_MAX_AUDIO_CLIP_FILESIZE_MB", "25") ), diff --git a/vllm/multimodal/media/audio.py b/vllm/multimodal/media/audio.py index 1f5a41c9df..34a3da32fb 100644 --- a/vllm/multimodal/media/audio.py +++ b/vllm/multimodal/media/audio.py @@ -312,6 +312,7 @@ class AudioMediaIO(MediaIO[tuple[npt.NDArray, float]]): return self.load_bytes(pybase64.b64decode(data)) def load_file(self, filepath: Path) -> tuple[npt.NDArray, float]: + self._validate_encoded_size(filepath.stat().st_size) return load_audio( filepath, sr=None,