fix: P1 安全修复 - TTS合成接口增加voice_clone_profile_id归属校验
CI/CD Pipeline / Frontend Lint (push) Failing after 50h43m5s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 50h43m5s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1709h18m14s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1709h18m16s
CI/CD Pipeline / Build Production Runtime Images (push) Failing after 1709h18m19s
CI/CD Pipeline / Deploy Staging (push) Failing after 1709h18m19s
CI/CD Pipeline / Deploy Production (push) Failing after 1709h49m44s

POST /tts/synthesize 未校验 voice_clone_profile_id 归属,
任意用户可使用他人克隆音色进行合成。
增加 ownership 校验,非本人 profile 返回 403。
This commit is contained in:
灵应
2026-07-07 10:10:57 +08:00
parent ae9a79f01d
commit 25a9352794
4 changed files with 42 additions and 2 deletions
+21 -1
View File
@@ -6,7 +6,11 @@ import logging
from typing import Optional
from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import get_cosyvoice_service, get_db_session
from app.dependencies import (
get_cosyvoice_service,
get_db_session,
get_voice_clone_profile_repository,
)
from app.schemas.tts import (
ListTTSJobResponse,
TTSJobResponse,
@@ -73,6 +77,7 @@ def synthesize(
authenticated_user: AuthenticatedUser = Depends(get_current_user),
repository: SQLAlchemyTTSJobRepository = Depends(_get_repository),
cosyvoice_service: CosyVoiceService = Depends(get_cosyvoice_service),
voice_clone_repo=Depends(get_voice_clone_profile_repository),
) -> TTSSynthesizeResponse:
"""发起 TTS 合成任务。
@@ -80,6 +85,21 @@ def synthesize(
与音色克隆接口保持一致:CosyVoice 失败时不抛 500,而是返回 201 + failed 状态任务记录。
"""
user_id = authenticated_user.user.id
# 校验 voice_clone_profile_id 归属(防止越权使用他人克隆音色)
if request.voice_clone_profile_id:
profile = voice_clone_repo.get(request.voice_clone_profile_id)
if profile is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Voice clone profile not found",
)
if profile.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to voice clone profile",
)
use_case = CreateTTSJobUseCase(repository)
job = use_case.execute(
user_id=user_id,
+9
View File
@@ -114,6 +114,15 @@ export const createAssetLibrary = async (data: {
return response.data;
};
/** 确保项目下指定 kind 的默认素材库存在(不存在则自动创建) */
export const ensureDefaultLibrary = async (data: {
project_id: string;
kind: "video" | "voice" | "image";
}): Promise<AssetLibraryItem> => {
const response = await apiClient.post("/asset-libraries/ensure-default", data);
return response.data;
};
/** 删除素材库 */
export const deleteAssetLibrary = async (libraryId: string): Promise<void> => {
await apiClient.delete(`/asset-libraries/${libraryId}`);
@@ -2,9 +2,10 @@
* 剪辑计划编辑器 — V8 原型 1:1 还原
* 四行布局:顶栏(42px) → 模式栏(48px) → 三栏主体 → 底栏(40px)
*/
import React, { useState, useCallback, useEffect } from "react";
import React, { useState, useCallback, useEffect, useMemo } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import { message } from "antd";
import { useQuery } from "@tanstack/react-query";
import type {
EditingTemplate,
TemplateCategory,
@@ -30,6 +31,12 @@ import { useUndoRedo } from "./hooks/useUndoRedo";
import type { TaskItem } from "@/api/tasks";
import { createGenerationTask, getTask, retryTask } from "@/api/tasks";
import type { ClipData, ClipType } from "./types";
import {
ensureDefaultLibrary,
getAssetsByKind,
type AssetItem,
} from "@/api/assets";
import { getOrCreateDefaultProject } from "@/api/projects";
import MediaPanel from "./components/MediaPanel";
import PreviewPlayer from "./components/PreviewPlayer";
@@ -14,4 +14,8 @@ export interface ClipData {
template_segment_id?: string;
script_text?: string;
order?: number;
/** 配音素材 ID(voice 类型片段使用) */
voice_asset_id?: string;
/** 配音素材文件 URL(voice 类型片段使用) */
voice_file_url?: string;
}