在线点播系统 MeEdu 发布了 v4.9.31,这次更新只有一个核心主题——安全。一个被 CNVD 收录的漏洞揭示:当 .env 中没有配置 JWT_SECRET 时,系统会回退到代码里写死的默认值。任何知道这个默认值的人,都能直接伪造管理员 Token,拿到完整后台权限。
这不是理论风险,是实打实能被利用的问题。版本同时引入了一个破坏性变更来堵住它:升级后如果 .env 里没有 JWT_SECRET,服务直接拒绝启动。
漏洞根因:回退逻辑比没有密钥更危险
很多 Laravel 项目用 tymon/jwt-auth 或类似包来处理 Token。正常流程是这样的——从 .env 读取密钥,签发和验证 Token:
// 正常路径:密钥来自环境变量
$secret = config('jwt.secret');
问题出在"密钥没配置"时的处理方式。MeEdu 旧版本的逻辑是:如果 .env 里找不到 JWT_SECRET,就回退到一个硬编码的默认字符串,让系统继续跑起来。开发者可能觉得这只是本地开发的便利,但实际部署时,只要有人忘了配这个变量,生产环境就在用一把所有人都知道的钥匙。
攻击者只需要:
- 找到源码或文档中的默认密钥值(开源项目这步毫无门槛)
- 用这把密钥签发一个
role: admin的 JWT - 拿着这个 Token 调用任何管理员 API
整个过程不需要任何凭证,不需要数据库访问,不需要网络层突破。一个 HTTP 请求就够了。
破坏性变更:宁可启动失败,也不用默认密钥
v4.9.31 的修复策略很直接——删掉回退逻辑。现在如果 JWT_SECRET 缺失,Laravel 启动时直接抛异常,服务不跑。
这意味着升级后你会遇到这个场景:
# 升级后启动,如果 .env 里没有 JWT_SECRET
php artisan serve
# 报错,服务无法启动
# RuntimeException: JWT_SECRET must be set in .env file
这不是 bug,是设计意图。一个能被伪造管理员 Token 的系统,比一个启动失败的系统危险得多。
升级实操:三步走,先生成密钥再部署
升级前做好准备,避免部署时服务中断:
第一步:在当前环境生成密钥
# 在升级代码之前,先执行这个命令
php artisan jwt:secret
# 输出类似:
# jwt-auth secret [XXXX] set successfully.
这条命令会在 .env 文件中写入一个随机生成的 JWT_SECRET,同时更新 .env.example。执行后用 cat .env | grep JWT_SECRET 确认值已写入。
第二步:处理已有用户的 Token
密钥更换后,所有用旧密钥签发的 Token 立即失效——包括已登录用户的会话。如果你的系统有大量在线用户,需要考虑过渡策略:
# 方案 A:直接换密钥,用户重新登录(最简单,最安全)
php artisan jwt:secret
# 方案 B:如果旧密钥确实就是硬编码默认值,没有任何安全意义
# 直接换掉就行,旧 Token 本来就不该被信任
如果旧 .env 里本来就没有 JWT_SECRET(意味着之前就在用硬编码默认值),那旧 Token 本身就是不安全的,直接换密钥、让用户重新登录是唯一正确的做法。
第三步:拉取新代码,部署
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate # 如果有数据库变更
php artisan config:cache # 缓存配置,确保新密钥被加载
php artisan route:cache
php artisan view:cache
部署完成后验证服务正常启动:
# 快速验证
php artisan up
curl -s -o /dev/null -w "%{http_code}" https://your-domain/api/health
# 期望返回 200
更广泛的教训:所有配置回退都是潜在漏洞
MeEdu 这次的问题不是个例。任何"配置缺失 → 回退到硬编码默认值"的模式,在安全相关场景下都是隐患。几个值得自查的点:
# 检查你的 Laravel 项目中是否有类似回退
grep -rn "env\(" config/ | grep -i "secret\|key\|password\|token"
# 常见的危险模式:
# 'secret' => env('JWT_SECRET', 'hardcoded-default-value'),
# 'key' => env('APP_KEY', 'SomeDefaultKey1234567890'),
#
# 安全模式:
# 'secret' => env('JWT_SECRET'), // 缺失时返回 null,启动失败
自查清单:
| 检查项 | 安全做法 |
|---|---|
JWT_SECRET |
必须在 .env 中设置,无回退默认值 |
APP_KEY |
Laravel 已强制要求,但确认不是从模板复制的示例值 |
| 数据库密码 | 不允许空密码回退,尤其生产环境 |
| 第三方 API Key | 缺失时功能降级,而非用假 Key 继续跑 |
| 加密密钥 | 缺失时拒绝执行加密/解密操作 |
一个简单的原则:凡是用来签名、加密、鉴权的密钥,缺失时的正确行为是"停止工作",而不是"找个替代值继续跑"。 MeEdu v4.9.31 用一个破坏性变更执行了这个原则,代价是一次部署时的额外操作,换来的是堵住了一个能被远程利用的管理员权限漏洞。