mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-10-25 17:36:29 +08:00
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:
1 parent
9e8eb1192e
commit
75c8f53cfc
2 files changed
+33
-5
No files matched your search
@@ -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
|
||||
@@ -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}")
|
||||
|
||||
Reference in new issue
Block a user