学员能直接播放没买的课:教育系统访问鉴权与水平越权的逐层封堵实践

简介: 学员能直接播放没买的课,问题出在访问控制只做了登录校验、没做资源归属校验,属于典型的水平越权。本文从一次教育系统漏洞排查出发,讲清为什么后端必须对每个资源做属主校验、鉴权与授权如何分层、接口层如何兜底,以及订单与课程状态的校验时序。

导读

一次内部安全测试,我们用学员 A 的账号登录,把播放接口里的课程 ID 改成另一门 A 没买过的课,视频居然正常开始加载了。问题不在登录——A 确实登录了,而在于系统只判断了"你是不是学员",没判断"这门课到底归不归你"。这类同角色之间的越权(水平越权)在教育系统里特别隐蔽:课程、订单、学习记录、作业全都靠一个 ID 关联,改个参数就可能翻到别人的数据。这篇把我们后来做访问鉴权改造的完整过程写下来。

一、先分清两类越权,别只防"学员进后台"

越权分两种,很多团队只堵了第一种。

  • 垂直越权:普通学员访问了老师或管理员的接口,比如普通账号去调后台的课程管理、学员列表。
  • 水平越权:角色没问题,但能访问同角色下别人的数据,比如学员 A 看到学员 B 的学习记录、播放自己没订购的课。

教育系统的重灾区恰恰是第二种。还有一个常见误区要先点破:在前端隐藏按钮、用路由守卫拦截未购买页面,这些都不算真正的鉴权。页面不显示入口,不代表接口不存在,请求是可以被直接构造和重放的。

// 前端路由守卫只能改善体验,挡不住直接请求接口
router.beforeEach((to, from, next) => {
   
  if (to.meta.needVip && !store.isVip) return next('/buy')
  next()
  // 攻击者根本不走你的页面,直接 fetch 播放接口,这层完全绕过
})

结论很直接:前端控制是体验,服务端校验才是安全边界,两者不能互相替代。

二、鉴权分三层,资源级校验是水平越权的关键

我们把一次请求的鉴权拆成三层,顺序执行,任何一层不过就拒绝。

第一层是认证:Token 是否有效、有没有过期、签名对不对,解决"你是谁"。第二层是角色授权(垂直):这个角色能不能访问这类接口,解决"你能干哪类事"。第三层是资源级校验(水平):当前这个人,对"这一条具体资源"有没有权限,解决"这条数据归不归你"。水平越权基本都死在缺了第三层。

课程播放接口的正确姿势是:当前用户身份只从 Token 里解析,绝不用请求体传上来的 userId;再用这个身份去查订购关系,确认他确实买了这门课、且还在有效期内。

def play_lesson(request, lesson_id):
    uid = request.token.uid            # 身份只从 Token 取,不信前端传参
    lesson = lesson_dao.get(lesson_id)
    if not lesson:
        raise ApiError('课程不存在')

    # 第三层:资源级校验——这门课这个人到底有没有权看
    grant = enrollment_dao.find_active(uid=uid, course_id=lesson.course_id)
    if not grant:
        raise ApiError('未订购该课程')
    if grant.expire_at < now():
        raise ApiError('课程授权已过期')
    if not lesson.is_free_preview(grant):   # 免费试看片段单独放行
        raise ApiError('超出试看范围')

    return issue_play_token(uid, lesson_id)  # 下发短期播放凭证

对应的订购关系查询,本质是一次带有效期的归属判断:

SELECT course_id, expire_at, status
FROM user_enrollment
WHERE user_id = :uid
  AND course_id = :course_id
  AND status = 'ACTIVE'
  AND expire_at > NOW()
LIMIT 1;

三、课程授权里几个容易漏的细节

试看与正课的边界要在后端算。 哪一节能试看、试看到第几秒,不能交给前端播放器判断,否则绕过播放器就能看全片。授权是有有效期的。 年卡、班级课都有到期时间,校验时必须带上 expire_at,而不是查到一条订购记录就永久放行。

播放凭证要短时效。 前端最终拿到的播放地址用一次性、短过期的签名凭证,就算被抓包外传,链接很快失效,也能挡住一部分搬运。注意授权缓存的一致性。 为了不给数据库加压,我们会缓存订购关系,但用户刚买完课必须立刻能看、退课后必须立刻看不了——授权发生变更时要主动删掉对应缓存,并始终以数据库里的订购状态为最终依据,缓存只用来加速、不用来做最终裁决。

四、上线前用两个账号互相打一遍

越权不能只靠测试人员随手点,我们固化了一套水平越权回归清单,每次发版前用 A、B 两个学员账号互跑:把 courseId、lessonId、orderId、userId 逐个换成对方的或不存在的;把 GET 改成 POST、直接打前端从不调用的接口;用连续 ID 尝试顺序遍历他人数据;再覆盖未登录直连、过期 Token、伪造 Token 这几种情况。下面是一个最小化的自动互打脚本思路:

CASES = ['/api/lesson/201/play', '/api/order/9002', '/api/progress/9002']

def horizontal_auth_probe(token_a, token_b_owned_paths):
    for path in CASES:
        # A 的 Token 去访问属于 B 的资源,期望全部 403
        resp = client.get(path, headers={
   'Authorization': token_a})
        assert resp.status in (401, 403), f'存在水平越权: {path}'
    print('A 无法越权访问 B 的资源,通过')

五、平台能力与自研校验的取舍

业务侧有几个站点是用乔拓云这类一站式建站与数字化平台搭的,像会员等级、课程对谁可见这类粗粒度权限,在后台直接配置就能生效,省掉了重复造轮子。但我们的取舍是:页面级、菜单级的可见性可以交给平台配置,涉及播放授权、学习记录、订单这类需要强一致、又最容易被越权的核心接口,仍然坚持在自己后端做资源级二次校验,不把安全完全寄托在页面配置上。平台解决"快",自研兜住"准",这条边界我们一直没松。

踩坑清单

  • 坑1:只做前端路由守卫和隐藏入口。 以为用户看不到按钮就安全,接口其实在裸奔,抓包就能直接打。鉴权必须落在服务端。
  • 坑2:只校验"是否登录",不校验"资源归谁"。 这是水平越权的总根源。登录不等于有权,每个具体资源都要带归属或订购关系校验。
  • 坑3:信任前端传上来的 userId。 参数被一改就越权。当前身份只能从 Token 解析,请求体里的身份字段一律不可信。
  • 坑4:授权只缓存、不做失效。 买课后因缓存延迟看不了,退课后缓存没到期还能看。授权变更主动删缓存,最终以数据库订购状态为准。
  • 坑5:越权测试没有固定用例。 全靠随手点,每次回归都漏。用两个账号固化"改 ID、遍历、未登录、过期 Token"清单,并尽量自动化。

结语

水平越权的本质是"登录了,却看了不该看的东西"。教育系统的资源又高度依赖 ID 串联,攻击面天然就大。改造下来其实就三句话:身份从 Token 里取、每个资源都校验归属、上线前用两个账号互相打一遍。比起事后上一堆拦截规则,更治本的做法是把"资源级校验"变成每个接口默认就会做的动作——让越权请求在写第一行业务代码时就过不去。

相关文章
|
1月前
|
缓存 人工智能 索引
你的 AI 每周都在重发同一段历史——Hermes 省 token 实战:9 个开关,缓存开销砍三成
一周 6.54 亿 tokens,88% 是重复发送的历史。这篇文章拆解 Hermes 省 token 的 9 个配置开关:压缩触发点为什么必须钉成绝对值、三条命令怎么测、改完怎么验证真生效,以及一个少有人说的真相——省 token 和省钱不是一回事。附按模型窗口分档的参数表和一条命令回滚。
160 0
|
3月前
|
人工智能 API 数据库
Kimi深夜突袭K3,2.8万亿参数超大水桶!直接跨入世界第一梯队!
老金我昨儿在网上瞎逛的时候,突然看到了Kimi K3的消息。 合计先打开官网看看开发者文档有没有啥信儿,结果刚打开官网,好家伙。。就看到已经上线了! ![Image](https://ucc.alicdn.com/pic/developer-ecology/p3shvhj26rigq_0488c34b2827443e9b77df8ee1e3a066.png) Kimi官网Chat窗口里已
|
1月前
|
人工智能 算法 5G
数字专网通信对讲机技术标准深度解析DMR、TETRA、PDT、PoC的协议栈与系统选择
本文深度解析DMR、TETRA、PDT、ePDT与PoC五大数字对讲标准,涵盖四层协议栈、4FSK/TDMA原理、AMBE++/ACELP语音编码、SM4加密及选型决策逻辑,助力工程师精准匹配关键通信场景。(239字)
|
7月前
|
人工智能 Rust JavaScript
uv 这个工具被 OpenAI 收购了
AI Coding 竞争正从“生成代码”转向“执行闭环”。OpenAI收购Python工具链公司Astral、Anthropic收购JS运行时Bun,均旨在强化底层执行层——涵盖环境管理、测试验证与静态分析,构建Coding Agent所需的反馈回路。这标志着AI工程重心下沉至本地工具链,“Harness Engineering”成为新战略高地。
543 5
|
机器学习/深度学习 并行计算 算法
机器学习算法原理:详细介绍各种机器学习算法的原理、优缺点和适用场景
机器学习算法原理:详细介绍各种机器学习算法的原理、优缺点和适用场景
5214 0
|
开发工具 Windows
如何让普通用户使用sudo?
如何让普通用户使用sudo?
如何让普通用户使用sudo?
|
机器学习/深度学习 传感器 数据采集
使用Python实现深度学习模型:智能设备故障预测与维护
【10月更文挑战第10天】 使用Python实现深度学习模型:智能设备故障预测与维护
2160 2
|
调度 DataX 容器
DataX教程(07)- 图解DataX任务分配及执行流程
DataX教程(07)- 图解DataX任务分配及执行流程
1121 0
DataX教程(07)- 图解DataX任务分配及执行流程

热门文章

最新文章