# RailwaySystem 数据范围与 IDOR 安全防护设计 (Data Scope & IDOR Prevention) - **设计版本**: R.2A Design Freeze - **设计日期**: 2026-09-15 - **核心目标**: 建立从安全凭证(`SecurityContext`)到业务主体 ID 的单向信任推导链条,杜绝前端参数篡改越权,消除全站直接对象引用(IDOR)安全漏洞。 --- ## 1. 数据范围核心设计原则 (Zero-Trust Frontend) 在多角色(Admin, Teacher, Student)登录体系中,数据范围安全必须建立在**零信任前端参数**的基石之上: ``` +─────────────────────────────────────────────────────────────────────────────+ | 零信任推导核心防线 (Secure Chain) | | | | 客户端 HTTP Request | | │ (仅携带 Bearer JWT Token,禁止/忽略 query 或 body 中的 identityId) | | ▼ | | Spring Security Filter: JwtAuthenticationTokenFilter | | │ (验签并提取 claims 中的 userId) | | ▼ | | SecurityContextHolder.getContext().getAuthentication() | | │ (获取受信任的 LoginUser.getUserId()) | | ▼ | | Service 内部数据推导 (SecurityContextHelper) | | ├─ 若拥有 train_teacher 角色: | | │ SELECT t_id FROM train_teacher WHERE user_id = #{userId} | | ├─ 若拥有 train_student 角色: | | │ SELECT s_id FROM train_student WHERE user_id = #{userId} | | └─ 若拥有 admin / train_admin 角色: | | 按管理员角色规则放行或基于组织机构过滤 | +─────────────────────────────────────────────────────────────────────────────+ ``` ### 1.1 核心铁律 1. **禁止直接接收外部身份 ID**: 在教师和学员的私有业务接口中,接口方法参数严禁声明 `@RequestParam String studentId` 或 `@PathVariable String teacherId`。 2. **强制服务端单向解析**: 所有业务查询条件中的 `studentId`、`teacherId` 必须统一在 Service 事务内由 `SecurityUtils.getUserId()` 动态查询或注入。 3. **严格归属校验 (Ownership Assertion)**: 在涉及具体资源主键操作(如查看某个具体 `reportId`、`checkinId`、`enrollmentId`)时,SQL 中必须强制拼接 `AND student_id = #{currentStudentId}` 或 `AND teacher_id = #{currentTeacherId}`,不满足条件时直接返回 `404 Not Found` 或 `403 Forbidden`,坚决不泄露数据存在性。 --- ## 2. 教师 (Teacher) 业务数据范围推导链条 教师登录后,其可访问的所有教学资源必须严格基于其受聘授课关系动态推导: ``` ┌──────────────────────┐ │ sys_user (userId) │ └──────────┬───────────┘ │ WHERE user_id = #{userId} ▼ ┌──────────────────────┐ │ train_teacher (t_id)│ └──────────┬───────────┘ │ ┌───────────────────────┼────────────────────────┐ │ (排课直接授课) │ (计划课程关联) │ (关联教学班级) ▼ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │timetable_lesson │ │train_plan_course │ │ train_class_plan │ │WHERE teacher_id │ │WHERE teacher_id │ │ WHERE plan_id IN │ │ = #{t_id} │ │ = #{t_id} │ │ (授课班级集合) │ └────────┬─────────┘ └──────────────────┘ └────────┬─────────┘ │ │ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │student_checkin │ │training_enrollment │(本人课次考勤流水) │ │(我带的班级学员名单)│ └──────────────────┘ └──────────────────┘ ``` - **授课课表数据**: 仅能查到 `train_timetable_lesson.teacher_id = #{currentTeacherId}` 的课次。 - **关联班级数据**: 仅能查到由当前教师担任授课任务的 `plan_id` 集合。 - **班级学员名单**: 仅能读取自己任教班级名单,**严禁读取全校其他培训班的学员档案**,且读取名单时必须对身份证号、住址等个人隐私字段进行脱敏(仅显示后四位)。 --- ## 3. 学员 (Student) 私有数据范围推导链条 学员登录后,进入极简个人培训中心,数据范围完全收敛于“本人”: ``` ┌──────────────────────┐ │ sys_user (userId) │ └──────────┬───────────┘ │ WHERE user_id = #{userId} ▼ ┌──────────────────────┐ │ train_student (s_id)│ └──────────┬───────────┘ │ ┌─────────────┬───────────────┼───────────────┬─────────────┐ ▼ ▼ ▼ ▼ ▼ ┌───────┐ ┌──────────────┐ ┌───────────┐ ┌──────────────┐ ┌────────────┐ │ 个人 │ │ 参训班级名单 │ │ 个人报到 │ │ 个人考勤记录 │ │ 个人画像/ │ │ 档案 │ │ Enrollment │ │ Report │ │ Checkin │ │ 学时累计 │ │(只读) │ │WHERE s_id=...│ │WHERE s_id │ │WHERE s_id=...│ │WHERE s_id │ └───────┘ └──────┬───────┘ └───────────┘ └──────────────┘ └────────────┘ │ ▼ ┌──────────────┐ │ 班级参训课表 │ │(仅限正常在训)│ └──────────────┘ ``` - **学员档案**: 仅能读取与更新本人通讯电话、邮箱。 - **学员课表**: 仅能读取本人已分班入班(且状态为待报到、已报到或已完成)的班级课次,严禁获取其他班级的教学日程。 - **考勤与报到**: 报到记录、打卡记录、学时累计与学员画像全部以 `student_id = #{currentStudentId}` 为严格物理隔离边界。 --- ## 4. 全站 IDOR 漏洞排查与加固矩阵 (IDOR_RISK_MATRIX) 本矩阵针对现有 Controller 方法进行逐一安全排查,明确其在多角色模式下的越权风险等级与防护要求: | 序号 | 业务接口端点 | 当前实现机制 | 越权风险评定 | 潜在越权利用场景 | R.2 加固与防护措施 | | :---: | :--- | :--- | :---: | :--- | :--- | | 01 | `GET /train/student/{sId}` | 直接根据路径参数 `sId` 查询并返回档案明细 | **AT_RISK (高危)** | 学员登录后篡改 `sId` 抓取全校任意职工身份证号与家庭隐私 | **管理端专有**:普通学员严禁访问此端点;学员本人查阅统一走 `/train/me/student` | | 02 | `GET /train/teacher/{tId}` | 直接根据路径参数 `tId` 查询教师档案 | **AT_RISK (中危)** | 任意登录人员枚举读取全校师资档案 | **管理端专有**;教师本人查阅统一走 `/train/me/teacher` | | 03 | `GET /train/plans/{planId}/timetable` | 直接根据 `planId` 查询整班课次 | **AT_RISK (高危)** | 未参训学员或未授课教师随意侦测其他班级排课安排 | 校验当前登录人是否在该计划的教师或学员名单内,否则直接拒止 | | 04 | `GET /train/plans/{planId}/enrollments` | 直接根据 `planId` 查询全班学员名册 | **AT_RISK (高危)** | 普通学员篡改 `planId` 窃取其他班级全部学员花名册及电话 | **严禁学员访问**;教师仅能查阅所带班级名册且敏感字段脱敏 | | 05 | `GET /train/enrollments/{enrollmentId}` | 直接根据主键 `enrollmentId` 查询 | **AT_RISK (中危)** | 水平越权读取其他学员入班凭证 | 增加 `student_id = currentStudentId` 强行归属校验 | | 06 | `POST /train/attendance/sign-in` | Body 接收 `studentId`, `lessonId` | **AT_RISK (高危)** | 学员 A 抓包伪造请求体中的 `studentId`,为旷课学员 B 代签 | 接口服务端强制断言 `body.studentId == currentStudentId`,否则拦截 | | 07 | `PUT /train/attendance/{checkinId}` | 接收 `checkinId` 与修改状态 | **AT_RISK (高危)** | 普通学员或无关人员抓包篡改他人考勤记录 | 仅限拥有 `train:attendance:edit` 的管理员或授权教师操作 | | 08 | `GET /train/checkin/{checkinId}` | 直接按 ID 查询单条打卡记录 | **AT_RISK (低危)** | 水平遍历探测其他人员打卡位置与时间 | 增加当前登录人身份归属断言 | | 09 | `GET /train/portrait/{portraitId}` | 直接按画像 ID 返回综合能力雷达 | **AT_RISK (高危)** | 学员枚举读取其他学员测评与能力短板数据 | 学员查阅统一走 `/train/me/portrait`,底层绑定本人 ID | | 10 | `GET /train/duration/{durId}` | 直接按主键读取学时记录 | **AT_RISK (中危)** | 水平越权探测他人学时与上线情况 | 学员统一走 `/train/me/duration`,强制本人绑定 | | 11 | `POST /train/plans` (新增计划) | `@PreAuthorize` 鉴权 | `SAFE` | 非管理员无权限直接返回 403 | 角色与菜单隔离即可防御 | | 12 | `POST /train/plans/{id}/lessons` (排课) | `@PreAuthorize` 鉴权 | `SAFE` | 非教务人员无排课权限 | 角色与功能权限隔离即可防御 | ### 4.1 审计总结 - **高危越权端点统计**: `IDOR_RISK_COUNT = 8`。 - **防护治理策略**: 1. **管理接口封闭**: 原有 `/train/student/**`、`/train/teacher/**`、`/train/enrollments/**` 仅保留给 `train_admin`。 2. **自服务通道独立**: 为 Teacher 与 Student 开辟全新的 `/train/me/**` API,彻底切断直接对象引用漏洞源。