From 75c8f53cfc24508b0b5699902a2ffad6cfa90918 Mon Sep 17 00:00:00 2001 From: c020627 <1326713348@qq.com> Date: Tue, 29 Sep 2026 22:41:53 +0800 Subject: [PATCH] fix(voice): cap DashScope TTS audio download size to avoid disk-fill DoS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DashScopeVoice._download_audio buffered the whole response body with no size limit, so a hostile or broken TTS audio URL could fill disk (probe P4 — unbounded media download). Route it through the size-capped common.media_download.download_to_file (100 MB cap); the import is aliased because this module already defines its own MAX_FILE_BYTES for ASR input. The method already degrades to None on any download failure, so an oversized response is refused the same way. Adds a regression test. Co-Authored-By: WorkBuddy --- tests/test_dashscope_voice_download.py | 31 ++++++++++++++++++++++++++ voice/dashscope/dashscope_voice.py | 7 ++---- 2 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 tests/test_dashscope_voice_download.py diff --git a/tests/test_dashscope_voice_download.py b/tests/test_dashscope_voice_download.py new file mode 100644 index 00000000..36765462 --- /dev/null +++ b/tests/test_dashscope_voice_download.py @@ -0,0 +1,31 @@ +# encoding:utf-8 +"""Bounded-download guard for ``DashScopeVoice._download_audio`` (probe P4). + +Regression guard: before the size-capped downloader was wired in, +``_download_audio`` buffered ``resp.content`` with no upper bound, so a hostile +or broken TTS audio URL could fill disk. The downloader now raises +``MediaTooLargeError`` past the cap, which the method degrades from (returns +None) like any other download failure. +""" +import requests +from common.media_download import MAX_FILE_BYTES +from unittest.mock import MagicMock, patch + +from voice.dashscope.dashscope_voice import DashScopeVoice + + +def test_download_audio_rejects_oversized_body(): + url = "http://example.test/audio.mp3" + resp = MagicMock() + resp.raise_for_status.return_value = None + # content is set only so the *old* code path (which wrote resp.content) + # can complete without a TypeError; the size-capped downloader aborts on + # the Content-Length pre-check before reading the stream. + resp.content = b"" + resp.headers = {"Content-Length": str(MAX_FILE_BYTES + 1)} + resp.close.return_value = None + + with patch.object(requests, "get", return_value=resp): + # On an oversized response the method must degrade to None rather than + # writing an unbounded body to disk. + assert DashScopeVoice._download_audio(url) is None diff --git a/voice/dashscope/dashscope_voice.py b/voice/dashscope/dashscope_voice.py index e1f0770c..97e25173 100644 --- a/voice/dashscope/dashscope_voice.py +++ b/voice/dashscope/dashscope_voice.py @@ -7,11 +7,11 @@ import random from typing import Optional import dashscope -import requests from dashscope import MultiModalConversation from bridge.reply import Reply, ReplyType from common.log import logger +from common.media_download import MAX_FILE_BYTES as _MEDIA_MAX_BYTES, download_to_file as _media_download_to_file from common.tmp_dir import TmpDir from config import conf from voice import audio_convert @@ -128,10 +128,7 @@ class DashScopeVoice(Voice): if ext not in (".mp3", ".wav", ".m4a", ".aac", ".opus"): ext = ".wav" dst = os.path.join(tmp_dir, f"dashscope_tts_{ts}_{random.randint(0, 9999)}{ext}") - resp = requests.get(url, timeout=60) - resp.raise_for_status() - with open(dst, "wb") as f: - f.write(resp.content) + _media_download_to_file(url, dst, _MEDIA_MAX_BYTES, timeout=60) return dst except Exception as e: logger.error(f"[DashScopeVoice] download audio failed: {e}")