fix(voice): cap DashScope TTS audio download size to avoid disk-fill DoS

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 <noreply@workbuddy.ai>
This commit is contained in:
c020627andWorkBuddy committed 2026-09-29 22:42:35 +08:00
1 parent 9e8eb1192e
commit 75c8f53cfc
2 files changed
+33 -5

No files matched your search

+31
View File
@@ -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
+2 -5
View File
@@ -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}")