# AI临床研究平台 安全评估报告与修复方案 > **文档类型**: 安全评估报告 + 漏洞修复方案 > **创建日期**: 2026-03-02 > **评估目标**: 北京市卫健委信息处安全审查准备 > **评估范围**: 代码安全、架构安全、阿里云 SAE 部署安全、数据安全与隐私合规 > **预计修复总工时**: P0 约 2-3 天,P1 约 1 周,P2 约 2 周 --- ## 目录 - [第一部分:安全问题总览](#第一部分安全问题总览) - [第二部分:当前系统已有的安全措施(可用于汇报)](#第二部分当前系统已有的安全措施可用于汇报) - [第三部分:P0 详细修复方案 — IDOR 水平越权](#第三部分p0-详细修复方案--idor-水平越权) - [第四部分:P0 详细修复方案 — 全局 LLM 脱敏](#第四部分p0-详细修复方案--全局-llm-脱敏) - [第五部分:分阶段安全加固路线图](#第五部分分阶段安全加固路线图) - [第六部分:阿里云安全产品采购建议](#第六部分阿里云安全产品采购建议) - [第七部分:安全架构总览(当前 vs 目标)](#第七部分安全架构总览当前-vs-目标) - [附录 A:LLM 调用点完整清单](#附录-allm-调用点完整清单) - [附录 B:安全制度文档编写清单](#附录-b安全制度文档编写清单) --- # 第一部分:安全问题总览 ## P0 — 致命风险(必须立即修复) ### 1. 生产环境全部凭据明文写在 Git 仓库的文档中 **这是最严重的安全问题。** `docs/05-部署文档/00-阿里云SAE最新真实状态记录.md` 中包含: - RDS 数据库密码 - ACR 镜像仓库密码 - OSS AccessKey / Secret - JWT Secret(完整 64 位) - 所有 LLM API Key(DeepSeek、DashScope、CloseAI、Unifuncs) - 企业微信 Corp Secret - 旧系统 MySQL 连接信息(IP + 用户名) **影响**:任何有 Git 仓库访问权限的人都能获取全部生产凭据。即使仓库是私有的,Git 历史会永久保留这些信息。 **修复**: - 立即将该文件从 Git 跟踪中移除,凭据部分改为引用(如"见阿里云控制台") - **轮换所有已暴露的密钥**(OSS AK/SK、JWT Secret、所有 API Key、数据库密码等) - 使用 `git filter-branch` 或 BFG Repo-Cleaner 清理 Git 历史 ### 2. `.env.example` 中包含疑似真实 API Key 文件 `backend/.env.example` 中: ``` DEEPSEEK_API_KEY=sk-7f8cc37a79fa4799860b38fc7ba2e150 DASHSCOPE_API_KEY=sk-75b4ff29a14a49e79667a331034f3298 ``` 示例文件应使用占位符(如 `your-key-here`),不应包含真实密钥。 ### 3. 多处脚本/测试代码硬编码 API Key 和密码 - `backend/src/modules/legacy-bridge/mysql-pool.ts:15` — MySQL 密码硬编码 - `backend/scripts/test-unifuncs-clinicaltrials.ts:11` — Unifuncs API Key 硬编码 - `backend/scripts/test-unifuncs-site-coverage.ts:13` — 同上 - `backend/src/modules/asl/__tests__/deep-research-chinese-sources.ts:10` — 同上 ### 4. SQL 注入漏洞 - `backend/src/modules/admin/iit-projects/iitQcCockpitController.ts` 第 211-214、233、244、301、543-556 行:`dateFilter`、`recordId`、`eventId`、`status`、`pageSize`、`offset` 等 query 参数**直接拼接到原始 SQL**,未使用参数化查询 - `backend/src/modules/pkb/services/VectorSearchService.ts` 第 125-165 行:`kbId`、`documentIds` 等仅做单引号转义,仍存在注入风险 ### 5. Legacy API 无认证保护 `backend/src/index.ts` 中以下路由**完全没有 `authenticate` 中间件**: - `/api/v1` 下的 `projectRoutes`、`agentRoutes`、`conversationRoutes`、`knowledgeBaseRoutes` - `/test/platform` — 暴露存储、缓存、队列等内部信息 任何人可以无需认证访问这些接口,读写数据。 ### 6. IDOR 水平越权漏洞(~29 个接口) 系统在处理资源查询、下载、操作时,部分模块**只校验了 Token 有效性,未校验资源是否属于当前用户**。攻击者遍历 ID 即可越权访问全站数据。 | 模块 | 受影响接口数 | 风险等级 | 涉及数据 | |------|------------|---------|---------| | SSA 智能统计分析 | ~12 | 高 | 用户上传的研究数据、分析代码、SAP 文档 | | DC Tool-C 数据清洗 | ~8 | 高 | 用户上传的 Excel/CSV 临床数据 | | ASL 文献提取 | ~5 | 高 | 提取结果、任务状态、导出文件 | | IIT CRA 质控 | ~4 | 中 | 跨租户项目数据(药企/医院) | > 详细修复方案见 [第三部分](#第三部分p0-详细修复方案--idor-水平越权) ### 7. LLM 调用普遍缺少 PII 脱敏 仅 DC 模块的 `DualModelExtractionService` 实现了 `maskPII()`。以下模块直接将原文发送给第三方 LLM: - RVW:稿件全文(可能含作者信息) - ASL:文献全文 - PKB:用户文档全文 - IIT:REDCap 记录(可能含患者信息) - SSA:用户上传的研究数据 **卫健委审查重点**:临床研究数据发送给外部 LLM 服务是否合规? > 详细修复方案见 [第四部分](#第四部分p0-详细修复方案--全局-llm-脱敏) --- ## P1 — 高风险(一周内修复) ### 8. CORS 配置允许任意来源 `backend/src/index.ts` 第 59 行:`origin: true`,允许任意域名跨域请求。代码中有 `config.corsOrigin` 配置项但未生效。 **修复**:生产环境改为白名单 `origin: ['https://iit.xunzhengyixue.com']` ### 9. 无速率限制(Rate Limiting) 登录、验证码发送、API 调用等均无速率限制,存在: - 暴力破解密码风险 - 短信验证码轰炸风险(虽有 1 分钟间隔,但无 IP 级限制) - API 滥用风险 ### 10. 验证码明文写入日志 `backend/src/common/auth/auth.service.ts` 第 419-421 行: ```typescript logger.info('验证码已生成', { phone, code, type, expiresAt }); console.log(`验证码: ${code}`); ``` 生产环境日志会被收集到 SLS,验证码可被任何有日志权限的人看到。 ### 11. 前端 Token 存储在 localStorage `frontend-v2/src/framework/auth/api.ts` 使用 `localStorage` 存储 accessToken 和 refreshToken,XSS 攻击可直接读取。 ### 12. Nginx 缺少安全响应头 `frontend-v2/nginx.conf` 未配置: - `X-Frame-Options`(防点击劫持) - `X-Content-Type-Options`(防 MIME 嗅探) - `Content-Security-Policy`(防 XSS) - `Strict-Transport-Security`(强制 HTTPS) ### 13. 无登录失败锁定机制 密码错误可无限次尝试,无任何锁定策略。 ### 14. 密码强度要求过低 `auth.service.ts` 第 322 行:仅要求长度 >= 6,无大小写、数字、特殊字符要求。 ### 15. CLB 负载均衡未配置 HTTPS 部署文档显示 CLB 仅监听 80 端口(HTTP),用户数据在公网以明文传输。 --- ## P2 — 中风险(两周内修复) ### 16. 用户 PII 明文存储 数据库 `users` 表中 `phone`(手机号)、`name`(姓名)、`email`(邮箱)均明文存储,未做列级加密或应用层加密。 ### 17. 多租户数据隔离不完整 - `requireSameTenant` 中间件仅在显式传 `tenantId` 时生效 - 多数业务接口只依赖 `userId`,未强制校验 `tenantId` - 业务数据表(Conversation、Document 等)通过 `userId` 关联,未在 Schema 层绑定 `tenant_id` ### 18. 管理员操作审计日志未启用 数据库中已有 `admin_operation_logs` 表,但代码中未找到写入逻辑。管理员的增删改操作无审计追踪。 ### 19. 登录日志缺少关键字段 当前登录日志不包含 IP 地址和 User-Agent,无法做异常登录分析。 ### 20. extraction_service 容器以 root 运行 `extraction_service/Dockerfile` 未创建非 root 用户,不符合最小权限原则。 ### 21. 错误响应泄露系统信息 - `ExtractionController.ts` 第 152 行:开发环境返回 error.stack - `FulltextScreeningController.ts` 第 301-304 行:返回 `task.errorStack` - 多个 Controller 返回 `error.message`,可能暴露内部实现 ### 22. 数据库连接未使用 SSL `DATABASE_URL` 中未配置 `sslmode=require`,数据库连接在网络层面可能被截获。 ### 23. 无自动化定期备份 当前依赖手工 `pg_dump`,根目录有多个 `.dump` 文件。RDS 有每日自动备份,但无应用层的定期备份策略。 --- # 第二部分:当前系统已有的安全措施(可用于汇报) ## 认证与授权 - JWT 认证体系(Access Token 2h + Refresh Token 7d) - 基于角色的访问控制(RBAC):SUPER_ADMIN / PROMPT_ENGINEER / ORG_ADMIN / USER - 模块级权限控制:`requireModule` 中间件 - 单设备登录机制:`tokenVersion` + 缓存实现踢人 - 密码使用 bcrypt 加密存储(rounds=10) ## 网络与基础设施 - 阿里云 VPC 网络隔离(172.17.0.0/16) - RDS 白名单限制:仅 VPC 网段可访问 - RDS 外网地址已关闭 - SAE 服务间通信走内网 - 后端 Dockerfile 使用非 root 用户(nodejs:1001) - 多阶段 Docker 构建,减少攻击面 - Nginx `server_tokens off`,隐藏版本信息 - Nginx 禁止访问隐藏文件和敏感后缀 ## 数据存储 - OSS 使用私有 Bucket + 签名 URL 访问 - OSS 通过内网 Endpoint 访问,减少数据泄露风险 - OSS 使用 HTTPS(`secure: true`) - 文件上传有 MIME 类型和大小限制 - 文件存储使用 UUID 命名,原始文件名存数据库 - 多 Schema 数据隔离(16 个独立 Schema) ## 应用安全 - Prisma ORM 参数化查询(大部分场景) - `.env` 文件在 `.gitignore` 中 - 生产环境 JWT Secret 强制不使用默认值 - Fastify 框架内置安全特性 - 运营行为日志(simple_logs) - RDS 自动备份 + PITR(每日凌晨 2 点,保留 7 天) --- # 第三部分:P0 详细修复方案 — IDOR 水平越权 ## 3.1 问题描述 系统中部分模块在处理资源查询、下载、操作时,**只校验了用户是否登录(Token 有效)**,未校验目标资源是否属于当前登录用户或其所在租户。攻击者只需遍历资源 ID,即可越权访问全站数据。 已正确实现权限校验的模块(PKB、RVW、AIA)证明项目中有成熟的做法可参考,问题出在各模块开发节奏不一致,未统一执行安全规范。 ## 3.2 已做好权限校验的模块(正面参考) 以下模块已正确实现 `userId` 归属校验,是修复其他模块的参考模板: **PKB 知识库** — `documentService.getDocumentById(userId, id)`: ```typescript // backend/src/modules/pkb/services/documentService.ts 第 202-206 行 const document = await prisma.document.findFirst({ where: { id: documentId, userId, // ✅ 强制校验归属 }, ``` **RVW 审稿** — `reviewService.getTaskDetail(userId, taskId)`: ```typescript // backend/src/modules/rvw/services/reviewService.ts 第 386-388 行 const task = await prisma.reviewTask.findFirst({ where: { id: taskId, userId }, // ✅ 强制校验归属 }); ``` **AIA 对话** — `conversationService.getConversationById(userId, id)`: ```typescript // backend/src/modules/aia/services/conversationService.ts 第 134-136 行 const conversation = await prisma.conversation.findFirst({ where: { id: conversationId, userId }, // ✅ 强制校验归属 }); ``` ## 3.3 漏洞详情与修复方案 ### 3.3.1 SSA 智能统计分析模块(~12 个接口,优先级最高) **问题根源**:所有 `ssaSession` 查询仅凭 `id`,未加 `userId` 条件。 **受影响文件与代码**: **(1) session.routes.ts** — 会话详情 / 消息 / 执行模式 ```typescript // backend/src/modules/ssa/routes/session.routes.ts 第 126-129 行 const session = await prisma.ssaSession.findUnique({ where: { id }, // ❌ 缺少 userId include: { messages: true } }); ``` ```typescript // 第 149 行 const session = await prisma.ssaSession.findUnique({ where: { id } }); // ❌ ``` ```typescript // 第 166-173 行 — 消息历史 const messages = await prisma.ssaMessage.findMany({ where: { sessionId: id }, // ❌ 未校验 session 归属 orderBy: { createdAt: 'asc' } }); ``` **(2) analysis.routes.ts** — 上传 / 计划 / 执行 / 下载代码 ```typescript // 第 54-58 行 — upload await prisma.ssaSession.update({ where: { id }, // ❌ 缺少 userId data: { dataOssKey: storageKey } }); ``` ```typescript // 第 73-77 行 — plan const session = await prisma.ssaSession.findUnique({ where: { id }, // ❌ select: { dataSchema: true } }); ``` ```typescript // 第 216-219 行 — execute const session = await prisma.ssaSession.findUnique({ where: { id } // ❌ }); ``` ```typescript // 第 334-338 行 — download-code const session = await prisma.ssaSession.findUnique({ where: { id }, // ❌ select: { title: true, createdAt: true } }); ``` **(3) consult.routes.ts** — 咨询聊天 / 生成 SAP / 下载 SAP ```typescript // 第 42-55 行 — chat:直接用 id 写入消息,未校验 session 归属 app.post('/:id/chat', async (req, reply) => { const { id } = req.params as { id: string }; await prisma.ssaMessage.create({ data: { sessionId: id, ... } // ❌ 未校验 session.userId }); ``` ```typescript // 第 142 行 — generate-sap / 第 168 行 — download-sap // 同样未校验 session 归属 ``` **(4) chat.routes.ts** — 对话历史 / 会话元信息(第 207-253 行) **修复方案**: ```typescript // 统一修复模式:所有 ssaSession 查询加 userId const userId = getUserId(request); const session = await prisma.ssaSession.findFirst({ where: { id: sessionId, userId }, }); if (!session) { return reply.status(404).send({ error: '会话不存在' }); } ``` 对于 `ssaMessage` 的查询,先校验 session 归属,再查消息: ```typescript const session = await prisma.ssaSession.findFirst({ where: { id: sessionId, userId }, }); if (!session) return reply.status(404).send({ error: '会话不存在' }); const messages = await prisma.ssaMessage.findMany({ where: { sessionId }, orderBy: { createdAt: 'asc' } }); ``` --- ### 3.3.2 DC Tool-C 数据清洗模块(~8 个接口) **问题根源**:`SessionService` 方法只接受 `sessionId`,不接受 `userId`。 **受影响文件**: - `backend/src/modules/dc/tool-c/controllers/SessionController.ts`(第 126-136 行) - `backend/src/modules/dc/tool-c/services/SessionService.ts`(第 148-155 行) ```typescript // SessionController.ts 第 131 行 const session = await sessionService.getSession(id); // ❌ 未传 userId // SessionService.ts 第 152-154 行 const session = await prisma.dcToolCSession.findUnique({ where: { id: sessionId }, // ❌ 缺少 userId }); ``` 受影响接口:`getSession`、`getPreviewData`、`getFullData`、`deleteSession`、`exportData`、`heartbeat`、`getUniqueValues`、`getStatus` **修复方案**: ```typescript // SessionService — 所有方法增加 userId 参数 async getSession(sessionId: string, userId: string): Promise { const session = await prisma.dcToolCSession.findFirst({ where: { id: sessionId, userId }, }); if (!session) throw new NotFoundError('会话不存在'); // ... } // SessionController — 从 request 取 userId 传入 const userId = getUserId(request); const session = await sessionService.getSession(id, userId); ``` --- ### 3.3.3 ASL 文献提取模块(~5 个接口) **问题根源**:Controller 直接用 `taskId`/`resultId` 查询,未校验 `task.userId`。 **受影响文件**: - `backend/src/modules/asl/extraction/controllers/ExtractionController.ts` - `backend/src/modules/asl/extraction/services/ExtractionService.ts` ```typescript // ExtractionController.ts 第 78-84 行 export async function getTaskStatus(request, reply) { const status = await extractionService.getTaskStatus(request.params.taskId); // ❌ } // ExtractionService.ts 第 118-122 行 async getTaskStatus(taskId: string) { const task = await prisma.aslExtractionTask.findUnique({ where: { id: taskId }, // ❌ 缺少 userId }); ``` ```typescript // ExtractionController.ts 第 119-136 行 — getResultDetail const result = await prisma.aslExtractionResult.findUnique({ where: { id: request.params.resultId }, // ❌ 未校验 result → task → userId include: { task: { ... } }, }); ``` 受影响接口:`getTaskStatus`、`getResults`、`getResultDetail`、`reviewResult`、`exportTaskResults`、`listDocuments` **修复方案**: ```typescript // ExtractionService — 增加 userId 参数 async getTaskStatus(taskId: string, userId: string) { const task = await prisma.aslExtractionTask.findFirst({ where: { id: taskId, userId }, }); if (!task) throw Object.assign(new Error('任务不存在'), { statusCode: 404 }); // ... } // getResultDetail — 通过关联 task 校验归属 const result = await prisma.aslExtractionResult.findFirst({ where: { id: resultId, task: { userId }, }, include: { task: { ... } }, }); ``` --- ### 3.3.4 IIT CRA 质控模块(~4 个接口,租户隔离) **问题根源**:项目查询仅凭 `id`,未按 `tenantId` 过滤,PHARMA_ADMIN / HOSPITAL_ADMIN 可能跨租户访问。 **受影响文件**: - `backend/src/modules/admin/iit-projects/iitProjectService.ts`(第 116-132 行) - `backend/src/modules/admin/iit-projects/iitBatchController.ts` - `backend/src/modules/admin/iit-projects/iitQcCockpitController.ts` ```typescript // iitProjectService.ts 第 117-118 行 async getProject(id: string) { const project = await this.prisma.iitProject.findFirst({ where: { id, deletedAt: null }, // ❌ 缺少 tenantId ``` **修复方案**: ```typescript async getProject(id: string, tenantId?: string) { const project = await this.prisma.iitProject.findFirst({ where: { id, tenantId, deletedAt: null }, }); // SUPER_ADMIN 可传 tenantId = undefined 跳过租户校验 ``` ## 3.4 注意事项 | 场景 | 处理方式 | |------|---------| | SUPER_ADMIN 需要查看所有用户数据 | 在 Service 方法中,若 role 为 SUPER_ADMIN 则跳过 userId/tenantId 校验 | | Worker / 定时任务等内部调用 | 保留不带 userId 的内部方法(标记为 `@internal`),仅在 HTTP API 入口层强制校验 | | IIT 项目多人协作 | 使用 tenantId 而非 userId 做隔离,同租户成员可共享访问 | | 查不到资源时的响应 | 统一返回 404(不返回 403,避免暴露资源是否存在) | ## 3.5 IDOR 修复工作量 | # | 任务 | 涉及文件 | 预估 | |---|------|---------|------| | 1 | SSA IDOR 修复 | session.routes.ts, analysis.routes.ts, consult.routes.ts, chat.routes.ts | 2-3h | | 2 | DC Tool-C IDOR 修复 | SessionController.ts, SessionService.ts | 1-2h | | 3 | ASL IDOR 修复 | ExtractionController.ts, ExtractionService.ts | 1-2h | | 4 | IIT IDOR 修复(tenantId) | iitProjectService.ts, iitBatchController.ts, iitQcCockpitController.ts | 1-2h | ## 3.6 IDOR 验证方法 1. 用户 A 创建 SSA 会话,记录 sessionId 2. 用户 B 直接请求 `GET /api/v1/ssa/sessions/{sessionId}` → 应返回 404 3. 用户 B 请求 `POST /api/v1/ssa/sessions/{sessionId}/upload` → 应返回 404 4. DC Tool-C、ASL 同理测试 --- # 第四部分:P0 详细修复方案 — 全局 LLM 脱敏 ## 4.1 问题描述 系统通过 `LLMFactory.getAdapter()` 调用外部大模型(DeepSeek、Qwen、GPT-5、Claude),共有 **32+ 调用点**分布在 12 个模块中。但 PII 脱敏仅在 DC 模块的 `DualModelExtractionService` 中实现,**其余所有模块直接将原始临床数据发送给外部 LLM**。 ## 4.2 当前脱敏覆盖状态 | 模块 | 脱敏 | LLM 调用点 | |------|------|-----------| | DC Tool-B(数据提取) | ✅ 有 maskPII | DualModelExtractionService.ts:149 | | DC Tool-C(数据清洗) | ❌ 无 | AICodeService.ts:89,395 / AIController.ts:244 | | SSA(智能统计分析) | ❌ 无 | AgentCoderService:57,100 / AgentReviewerService:54 / AgentPlannerService:63 / QueryService:105 等 7 处 | | IIT Manager(CRA 质控) | ❌ 无 | ChatOrchestrator:70 / SoftRuleEngine:214 | | ASL(文献检索) | ❌ 无 | requirementExpansionService:80 / llmScreeningService:68 / LLM12FieldsService:143 / ExtractionSingleWorker:151 | | PKB(知识库) | ❌ 无 | batchService:311 / chatController:231 | | RVW(审稿) | ❌ 无 | methodologyService:51 / editorialService:51 | | Agent Protocol | ❌ 无 | LLMServiceAdapter:45,91 | | 通用层 RAG / Streaming | ❌ 无 | QueryRewriter:33 / StreamingService:50 | | Legacy | ❌ 无 | conversationService:371,527 / batchService:311 / reviewService:216,268 / chatController:419 | ## 4.3 现有脱敏实现(可复用基础) DC 模块中已有的 `maskPII` 方法(`DualModelExtractionService.ts` 第 98-120 行): ```typescript private maskPII(text: string): string { let masked = text; // 手机号脱敏:138****5678 masked = masked.replace(/1[3-9]\d{9}/g, (match) => { return match.substring(0, 3) + '****' + match.substring(7); }); // 身份证号脱敏:330102********1234 masked = masked.replace( /\d{6}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dxX]/g, (match) => match.substring(0, 6) + '********' + match.substring(14) ); // 患者姓名脱敏 masked = masked.replace( /(患者|姓名[::])\s*([^\s,。,]{2,4})/g, (_, prefix, name) => prefix + name[0] + '*'.repeat(name.length - 1) ); return masked; } ``` ## 4.4 修复方案 ### 步骤 1:提取通用 PiiMaskingService 新建 `backend/src/common/services/piiMaskingService.ts`: ```typescript export class PiiMaskingService { /** * 对文本进行 PII 脱敏 * 覆盖:手机号、身份证号、患者姓名、邮箱地址 */ static maskText(text: string): string { let masked = text; // 手机号:138****5678 masked = masked.replace(/1[3-9]\d{9}/g, m => m.substring(0, 3) + '****' + m.substring(7)); // 身份证号:330102********1234 masked = masked.replace( /\d{6}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dxX]/g, m => m.substring(0, 6) + '********' + m.substring(14)); // 患者姓名 masked = masked.replace( /(患者|姓名[::])\s*([^\s,。,]{2,4})/g, (_, prefix, name) => prefix + name[0] + '*'.repeat(name.length - 1)); // 邮箱地址 masked = masked.replace(/[\w.-]+@[\w.-]+\.\w+/g, m => { const [local, domain] = m.split('@'); return local[0] + '***@' + domain; }); return masked; } /** * 对 LLM 消息数组脱敏(跳过 system 角色的 prompt) */ static maskMessages( messages: Array<{ role: string; content: string }> ): Array<{ role: string; content: string }> { return messages.map(m => ({ ...m, content: m.role === 'system' ? m.content : PiiMaskingService.maskText(m.content), })); } } ``` ### 步骤 2:在 LLMFactory 层挂载全局脱敏 Proxy 修改 `backend/src/common/llm/adapters/LLMFactory.ts`: ```typescript import { PiiMaskingService } from '../../services/piiMaskingService'; import { config } from '../../../config/env'; /** * 脱敏代理 — 包装真实 Adapter,在调用前自动对 messages 脱敏 */ class MaskedLLMAdapter implements ILLMAdapter { modelName: string; constructor(private inner: ILLMAdapter) { this.modelName = inner.modelName; } async chat(messages: Message[], options?: LLMOptions): Promise { return this.inner.chat(PiiMaskingService.maskMessages(messages), options); } async *chatStream( messages: Message[], options?: LLMOptions, onChunk?: (chunk: StreamChunk) => void ): AsyncGenerator { yield* this.inner.chatStream( PiiMaskingService.maskMessages(messages), options, onChunk ); } } // 在 getAdapter() 末尾: static getAdapter(modelType: ModelType): ILLMAdapter { // ... 原有 switch/case 逻辑不变 ... this.adapters.set(modelType, adapter); // 全局脱敏包装(通过环境变量控制开关) if (config.piiMaskingEnabled !== false) { return new MaskedLLMAdapter(adapter); } return adapter; } ``` > **注意**:由于 `getAdapter` 使用单例缓存,脱敏 Proxy 应在返回时包装,不应缓存 Proxy 本身(以便运行时切换开关)。 ### 步骤 3:DC 模块内的 maskPII 改为调用通用服务 修改 `backend/src/modules/dc/tool-b/services/DualModelExtractionService.ts`: ```typescript import { PiiMaskingService } from '../../../../common/services/piiMaskingService'; // 删除 private maskPII 方法,改为: const maskedText = PiiMaskingService.maskText(input.text); ``` ### 步骤 4:添加环境变量开关 在 `backend/src/config/env.ts` 中增加: ```typescript piiMaskingEnabled: process.env.PII_MASKING_ENABLED !== 'false', // 默认开启 ``` ## 4.5 脱敏影响评估与回退策略 | 风险 | 表现 | 缓解措施 | |------|------|---------| | 误匹配:医学编号被当手机号 | 如药物编号 `13812345678` 被脱敏为 `138****5678` | 在脱敏前排除已知格式(如 CAS 号、临床试验编号) | | LLM 回答质量下降 | 脱敏后语义受损 | 仅脱敏 user 消息,不动 system prompt | | 回退 | 线上紧急关闭 | 设置 `PII_MASKING_ENABLED=false` 环境变量即可关闭 | **灰度上线建议**: 1. 第一阶段:仅在 IIT 和 DC 模块启用(临床数据 PII 最密集) 2. 第二阶段:观察 1-2 天无异常后全量开启 ## 4.6 脱敏修复工作量 | # | 任务 | 涉及文件 | 预估 | |---|------|---------|------| | 1 | 通用 PiiMaskingService | 新建 common/services/piiMaskingService.ts | 0.5h | | 2 | LLMFactory 脱敏 Proxy | LLMFactory.ts | 1h | | 3 | DC maskPII 重构 | DualModelExtractionService.ts | 0.5h | | 4 | 环境变量 + 配置 | env.ts | 0.5h | ## 4.7 脱敏验证方法 1. 在任意模块(如 AIA 问答)发送包含手机号 `13800138000` 的消息 2. 检查后端日志中发送给 LLM 的实际内容,应为 `138****8000` 3. 设置 `PII_MASKING_ENABLED=false`,重启服务,确认脱敏关闭 --- # 第五部分:分阶段安全加固路线图 ## 第一阶段:紧急修复(部署前 1-3 天) - **凭据轮换**:更换所有已暴露在 Git 中的密钥(数据库密码、OSS AK/SK、JWT Secret、LLM API Key 等) - **修复 SQL 注入**:`iitQcCockpitController.ts` 和 `VectorSearchService.ts` 中的原始 SQL 改为参数化查询 - **IDOR 修复**:SSA、DC Tool-C、ASL、IIT 四个模块的水平越权漏洞(详见第三部分) - **全局 LLM 脱敏**:通过 LLMFactory Proxy 实现全模块脱敏(详见第四部分) - **CORS 白名单**:将 `origin: true` 改为生产域名白名单 - **关闭/保护 Legacy 路由**:为无认证的 Legacy API 添加认证或下线 - **关闭 `/test/platform` 路由**:生产环境禁用测试端点 - **Nginx 安全头**:添加 `X-Frame-Options`、`X-Content-Type-Options`、`X-XSS-Protection`、`Referrer-Policy` - **验证码日志脱敏**:生产环境不打印验证码 - **错误响应脱敏**:生产环境不返回 stack trace ## 第二阶段:加固措施(1-2 周) - **HTTPS 配置**:CLB 配置 SSL 证书,开启 443 端口,HTTP 强制跳转 HTTPS,Nginx 添加 HSTS - **速率限制**:使用 `@fastify/rate-limit` 对登录/验证码/API 添加限流 - **登录安全**:实现 5 次失败锁定 15 分钟;记录 IP 和 User-Agent - **密码策略强化**:长度 >= 8,包含大小写 + 数字 + 特殊字符 - **PII 加密**:对 `phone`、`name`、`email` 实施应用层 AES 加密存储 - **管理员审计日志**:启用 `admin_operation_logs` 表写入 - **数据库 SSL**:`DATABASE_URL` 添加 `sslmode=require` - **extraction_service 非 root**:Dockerfile 添加 `USER appuser` ## 第三阶段:阿里云安全产品与制度文档(2-4 周) - 阿里云安全产品采购(详见第六部分) - 安全制度文档编写(详见附录 B) --- # 第六部分:阿里云安全产品采购建议 | 产品 | 用途 | 优先级 | 预估成本 | |------|------|--------|---------| | **SSL 证书**(免费/付费) | HTTPS 加密传输 | P0 必选 | 免费(DV)或 ~2000/年(OV) | | **Web 应用防火墙(WAF)** | SQL 注入/XSS/CC 攻击防护 | P1 强烈建议 | ~3000/年起 | | **云安全中心(基础版免费)** | 漏洞扫描、基线检查、入侵检测 | P1 建议 | 免费基础版 | | **DDoS 防护(基础版免费)** | 防流量攻击 | P2 建议 | 免费基础版(5Gbps) | | **密钥管理服务(KMS)** | 密钥托管,替代环境变量存密钥 | P2 建议 | ~1000/年 | | **日志服务(SLS)** | 安全审计日志集中管理 | P2 建议 | 已有,按量 | | **RAM 精细化权限** | 最小权限原则 | P2 建议 | 免费 | | **操作审计(ActionTrail)** | 云资源操作审计 | P3 加分项 | 免费基础版 | --- # 第七部分:安全架构总览(当前 vs 目标) ```mermaid flowchart TB subgraph current [当前架构 - 存在风险] User1[用户浏览器] -->|HTTP 明文| CLB1[CLB:80] CLB1 --> Nginx1[Nginx - 无安全头] Nginx1 -->|HTTP| Backend1["Node.js - CORS:*"] Backend1 -->|"无SSL,明文密码在Git"| RDS1[RDS PostgreSQL] Backend1 -->|"PII明文"| LLM1[外部LLM] end subgraph target [目标架构 - 安全加固后] User2[用户浏览器] -->|HTTPS + HSTS| WAF[WAF防火墙] WAF --> CLB2[CLB:443 + SSL] CLB2 --> Nginx2["Nginx + 安全头 + CSP"] Nginx2 -->|HTTP内网| Backend2["Node.js + RateLimit + CORS白名单"] Backend2 -->|"SSL + KMS密钥"| RDS2[RDS PostgreSQL + PII加密] Backend2 -->|"PII脱敏后"| LLM2[外部LLM] Backend2 --> AuditLog[审计日志 → SLS] end ``` --- ## 总结 **当前安全等级评估:中低** 最紧迫的问题是**凭据泄露**(文档中明文记录所有生产密钥并提交到 Git)、**SQL 注入**漏洞和 **IDOR 水平越权**(~29 个接口无归属校验)。这些问题如果被利用,可能导致数据库被入侵、全部用户数据泄露。 **卫健委审查重点关注项**: 1. 数据传输加密(HTTPS)— 当前 CLB 仅 HTTP 2. 个人信息保护 — PII 明文存储,且未脱敏直接发送给第三方 LLM 3. 访问控制 — IDOR 水平越权可导致任意用户数据泄露 4. 临床数据发送给第三方 LLM 的合规性 5. 审计日志完整性 6. 数据备份与恢复能力 建议按 P0 → P1 → P2 优先级逐步修复,第一阶段的紧急修复可在 2-3 天内完成。 --- # 附录 A:LLM 调用点完整清单 以下为 `LLMFactory.getAdapter()` 的所有调用点,全局脱敏 Proxy 将自动覆盖: | 模块 | 文件 | 行号 | |------|------|------| | DC Tool-B | dc/tool-b/services/DualModelExtractionService.ts | 149 | | DC Tool-C | dc/tool-c/services/AICodeService.ts | 89, 395 | | DC Tool-C | dc/tool-c/controllers/AIController.ts | 244 | | SSA | ssa/services/AgentCoderService.ts | 57, 100 | | SSA | ssa/services/AgentReviewerService.ts | 54 | | SSA | ssa/services/AgentPlannerService.ts | 63 | | SSA | ssa/services/ReflectionService.ts | 95 | | SSA | ssa/services/QueryService.ts | 105 | | SSA | ssa/services/PicoInferenceService.ts | 52 | | SSA | ssa/services/IntentRouterService.ts | 181 | | SSA | ssa/services/ConversationService.ts | 253 | | IIT Manager | iit-manager/services/ChatOrchestrator.ts | 70 | | IIT Manager | iit-manager/engines/SoftRuleEngine.ts | 214 | | IIT Admin | admin/iit-projects/iitRuleSuggestionService.ts | 184 | | ASL | asl/services/requirementExpansionService.ts | 80 | | ASL | asl/services/llmScreeningService.ts | 68 | | ASL | asl/common/llm/LLM12FieldsService.ts | 143 | | ASL | asl/extraction/workers/ExtractionSingleWorker.ts | 151 | | PKB | pkb/services/batchService.ts | 311 | | PKB | pkb/controllers/chatController.ts | 231 | | RVW | rvw/services/methodologyService.ts | 51 | | RVW | rvw/services/editorialService.ts | 51 | | Agent Protocol | agent/protocol/services/LLMServiceAdapter.ts | 45, 91 | | 通用 RAG | common/rag/QueryRewriter.ts | 33 | | 通用 Streaming | common/streaming/StreamingService.ts | 50 | | Legacy | legacy/services/conversationService.ts | 371, 527 | | Legacy | legacy/services/batchService.ts | 311 | | Legacy | legacy/services/reviewService.ts | 216, 268 | | Legacy | legacy/controllers/chatController.ts | 419 | --- # 附录 B:安全制度文档编写清单 以下文档建议在安全审查前准备,用于汇报材料: 1. **《信息安全管理制度》** — 覆盖组织架构、职责分工、安全流程 2. **《数据分类分级管理制度》** — 临床研究数据属敏感数据,需分级管理 3. **《个人信息保护影响评估报告(PIA)》** — 手机号、姓名等 PII 的收集、使用、存储评估 4. **《数据安全应急预案》** — 数据泄露事件的响应流程 5. **《LLM 数据安全合规说明》** — 说明数据发送给哪些 LLM、是否脱敏、数据保留政策 6. **《等保三级自评清单》** — 卫健委可能要求