- 新增安全评估报告与P0修复方案(IDOR水平越权~29接口 + 全局LLM脱敏) - 新增安全开发规范v1.1(9大安全规范 + Code Review检查清单) - 采纳架构师审查意见补充OSS签名URL、导出审计、接口限流、依赖安全 - 更新开发规范README索引 Made-with: Cursor
32 KiB
AI临床研究平台 安全评估报告与修复方案
文档类型: 安全评估报告 + 漏洞修复方案
创建日期: 2026-03-02
评估目标: 北京市卫健委信息处安全审查准备
评估范围: 代码安全、架构安全、阿里云 SAE 部署安全、数据安全与隐私合规
预计修复总工时: P0 约 2-3 天,P1 约 1 周,P2 约 2 周
目录
- 第一部分:安全问题总览
- 第二部分:当前系统已有的安全措施(可用于汇报)
- 第三部分:P0 详细修复方案 — IDOR 水平越权
- 第四部分:P0 详细修复方案 — 全局 LLM 脱敏
- 第五部分:分阶段安全加固路线图
- 第六部分:阿里云安全产品采购建议
- 第七部分:安全架构总览(当前 vs 目标)
- 附录 A:LLM 调用点完整清单
- 附录 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 | 中 | 跨租户项目数据(药企/医院) |
详细修复方案见 第三部分
7. LLM 调用普遍缺少 PII 脱敏
仅 DC 模块的 DualModelExtractionService 实现了 maskPII()。以下模块直接将原文发送给第三方 LLM:
- RVW:稿件全文(可能含作者信息)
- ASL:文献全文
- PKB:用户文档全文
- IIT:REDCap 记录(可能含患者信息)
- SSA:用户上传的研究数据
卫健委审查重点:临床研究数据发送给外部 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 行:
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.stackFulltextScreeningController.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):
// backend/src/modules/pkb/services/documentService.ts 第 202-206 行
const document = await prisma.document.findFirst({
where: {
id: documentId,
userId, // ✅ 强制校验归属
},
RVW 审稿 — reviewService.getTaskDetail(userId, taskId):
// backend/src/modules/rvw/services/reviewService.ts 第 386-388 行
const task = await prisma.reviewTask.findFirst({
where: { id: taskId, userId }, // ✅ 强制校验归属
});
AIA 对话 — conversationService.getConversationById(userId, id):
// 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 — 会话详情 / 消息 / 执行模式
// backend/src/modules/ssa/routes/session.routes.ts 第 126-129 行
const session = await prisma.ssaSession.findUnique({
where: { id }, // ❌ 缺少 userId
include: { messages: true }
});
// 第 149 行
const session = await prisma.ssaSession.findUnique({ where: { id } }); // ❌
// 第 166-173 行 — 消息历史
const messages = await prisma.ssaMessage.findMany({
where: { sessionId: id }, // ❌ 未校验 session 归属
orderBy: { createdAt: 'asc' }
});
(2) analysis.routes.ts — 上传 / 计划 / 执行 / 下载代码
// 第 54-58 行 — upload
await prisma.ssaSession.update({
where: { id }, // ❌ 缺少 userId
data: { dataOssKey: storageKey }
});
// 第 73-77 行 — plan
const session = await prisma.ssaSession.findUnique({
where: { id }, // ❌
select: { dataSchema: true }
});
// 第 216-219 行 — execute
const session = await prisma.ssaSession.findUnique({
where: { id } // ❌
});
// 第 334-338 行 — download-code
const session = await prisma.ssaSession.findUnique({
where: { id }, // ❌
select: { title: true, createdAt: true }
});
(3) consult.routes.ts — 咨询聊天 / 生成 SAP / 下载 SAP
// 第 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
});
// 第 142 行 — generate-sap / 第 168 行 — download-sap
// 同样未校验 session 归属
(4) chat.routes.ts — 对话历史 / 会话元信息(第 207-253 行)
修复方案:
// 统一修复模式:所有 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 归属,再查消息:
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 行)
// 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
修复方案:
// 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.tsbackend/src/modules/asl/extraction/services/ExtractionService.ts
// 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
});
// 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
修复方案:
// 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.tsbackend/src/modules/admin/iit-projects/iitQcCockpitController.ts
// iitProjectService.ts 第 117-118 行
async getProject(id: string) {
const project = await this.prisma.iitProject.findFirst({
where: { id, deletedAt: null }, // ❌ 缺少 tenantId
修复方案:
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 验证方法
- 用户 A 创建 SSA 会话,记录 sessionId
- 用户 B 直接请求
GET /api/v1/ssa/sessions/{sessionId}→ 应返回 404 - 用户 B 请求
POST /api/v1/ssa/sessions/{sessionId}/upload→ 应返回 404 - 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 行):
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:
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:
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 本身(以便运行时切换开关)。
步骤 3:DC 模块内的 maskPII 改为调用通用服务
修改 backend/src/modules/dc/tool-b/services/DualModelExtractionService.ts:
import { PiiMaskingService } from '../../../../common/services/piiMaskingService';
// 删除 private maskPII 方法,改为:
const maskedText = PiiMaskingService.maskText(input.text);
步骤 4:添加环境变量开关
在 backend/src/config/env.ts 中增加:
piiMaskingEnabled: process.env.PII_MASKING_ENABLED !== 'false', // 默认开启
4.5 脱敏影响评估与回退策略
| 风险 | 表现 | 缓解措施 |
|---|---|---|
| 误匹配:医学编号被当手机号 | 如药物编号 13812345678 被脱敏为 138****5678 |
在脱敏前排除已知格式(如 CAS 号、临床试验编号) |
| LLM 回答质量下降 | 脱敏后语义受损 | 仅脱敏 user 消息,不动 system prompt |
| 回退 | 线上紧急关闭 | 设置 PII_MASKING_ENABLED=false 环境变量即可关闭 |
灰度上线建议:
- 第一阶段:仅在 IIT 和 DC 模块启用(临床数据 PII 最密集)
- 第二阶段:观察 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 脱敏验证方法
- 在任意模块(如 AIA 问答)发送包含手机号
13800138000的消息 - 检查后端日志中发送给 LLM 的实际内容,应为
138****8000 - 设置
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 目标)
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 个接口无归属校验)。这些问题如果被利用,可能导致数据库被入侵、全部用户数据泄露。
卫健委审查重点关注项:
- 数据传输加密(HTTPS)— 当前 CLB 仅 HTTP
- 个人信息保护 — PII 明文存储,且未脱敏直接发送给第三方 LLM
- 访问控制 — IDOR 水平越权可导致任意用户数据泄露
- 临床数据发送给第三方 LLM 的合规性
- 审计日志完整性
- 数据备份与恢复能力
建议按 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:安全制度文档编写清单
以下文档建议在安全审查前准备,用于汇报材料:
- 《信息安全管理制度》 — 覆盖组织架构、职责分工、安全流程
- 《数据分类分级管理制度》 — 临床研究数据属敏感数据,需分级管理
- 《个人信息保护影响评估报告(PIA)》 — 手机号、姓名等 PII 的收集、使用、存储评估
- 《数据安全应急预案》 — 数据泄露事件的响应流程
- 《LLM 数据安全合规说明》 — 说明数据发送给哪些 LLM、是否脱敏、数据保留政策
- 《等保三级自评清单》 — 卫健委可能要求