Files
AIclinicalresearch/docs/08-项目管理/05-技术债务/P0-IDOR水平越权修复与全局LLM脱敏方案.md
HaHafeng 0677d42345 docs(security): 安全评估报告 + IDOR修复方案 + 安全开发规范v1.1
- 新增安全评估报告与P0修复方案(IDOR水平越权~29接口 + 全局LLM脱敏)
- 新增安全开发规范v1.1(9大安全规范 + Code Review检查清单)
- 采纳架构师审查意见补充OSS签名URL、导出审计、接口限流、依赖安全
- 更新开发规范README索引

Made-with: Cursor
2026-03-03 22:20:03 +08:00

886 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AI临床研究平台 安全评估报告与修复方案
> **文档类型**: 安全评估报告 + 漏洞修复方案
> **创建日期**: 2026-03-02
> **评估目标**: 北京市卫健委信息处安全审查准备
> **评估范围**: 代码安全、架构安全、阿里云 SAE 部署安全、数据安全与隐私合规
> **预计修复总工时**: P0 约 2-3 天P1 约 1 周P2 约 2 周
---
## 目录
- [第一部分:安全问题总览](#第一部分安全问题总览)
- [第二部分:当前系统已有的安全措施(可用于汇报)](#第二部分当前系统已有的安全措施可用于汇报)
- [第三部分P0 详细修复方案 — IDOR 水平越权](#第三部分p0-详细修复方案--idor-水平越权)
- [第四部分P0 详细修复方案 — 全局 LLM 脱敏](#第四部分p0-详细修复方案--全局-llm-脱敏)
- [第五部分:分阶段安全加固路线图](#第五部分分阶段安全加固路线图)
- [第六部分:阿里云安全产品采购建议](#第六部分阿里云安全产品采购建议)
- [第七部分:安全架构总览(当前 vs 目标)](#第七部分安全架构总览当前-vs-目标)
- [附录 ALLM 调用点完整清单](#附录-allm-调用点完整清单)
- [附录 B安全制度文档编写清单](#附录-b安全制度文档编写清单)
---
# 第一部分:安全问题总览
## P0 — 致命风险(必须立即修复)
### 1. 生产环境全部凭据明文写在 Git 仓库的文档中
**这是最严重的安全问题。** `docs/05-部署文档/00-阿里云SAE最新真实状态记录.md` 中包含:
- RDS 数据库密码
- ACR 镜像仓库密码
- OSS AccessKey / Secret
- JWT Secret完整 64 位)
- 所有 LLM API KeyDeepSeek、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用户文档全文
- IITREDCap 记录(可能含患者信息)
- 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 和 refreshTokenXSS 攻击可直接读取。
### 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
- 基于角色的访问控制RBACSUPER_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<SessionData> {
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 ManagerCRA 质控) | ❌ 无 | 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<LLMResponse> {
return this.inner.chat(PiiMaskingService.maskMessages(messages), options);
}
async *chatStream(
messages: Message[],
options?: LLMOptions,
onChunk?: (chunk: StreamChunk) => void
): AsyncGenerator<StreamChunk, void, unknown> {
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 本身(以便运行时切换开关)。
### 步骤 3DC 模块内的 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 强制跳转 HTTPSNginx 添加 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 天内完成。
---
# 附录 ALLM 调用点完整清单
以下为 `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. **《等保三级自评清单》** — 卫健委可能要求