From 1c645cd8d34e22bdc35f7213e656dd1ee85fbc5b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 11:07:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(ad):=20=E6=96=87=E5=AD=97=E5=B9=BF=E5=91=8A?= =?UTF-8?q?=20cover=20=E5=88=97=20NOT=20NULL=20=E5=86=B2=E7=AA=81=EF=BC=9B?= =?UTF-8?q?=E5=88=A0=E9=99=A4=2011.txt=20=E6=9D=82=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - modules/cms/ad/service.py add_ad/update_ad:非图片广告(type!=1)cover 从 None 改为空字符串 '',避免插入 NOT NULL 列报 'cover cannot be null';详情展示已兼容空串 - 删除 11.txt(既有审查遗留的 bug 清单文件) Co-Authored-By: Claude --- 11.txt | 99 --------------------------------------- modules/cms/ad/service.py | 12 ++--- 2 files changed, 4 insertions(+), 107 deletions(-) delete mode 100644 11.txt diff --git a/11.txt b/11.txt deleted file mode 100644 index 535e3c2..0000000 --- a/11.txt +++ /dev/null @@ -1,99 +0,0 @@ -已按企业级标准完成全量审查(约 1.4 万行 Python,覆盖 25 个业务模块 + 核心框架)。以下按严重程度分类汇总。 - - --- - 一、严重 BUG(功能已损坏 / 需立即修复) - - 1. 上传接口全部 500 —— 缺 request 参数 - - apps/views/upload.py:15/23/31 三个端点签名是 async def upload_image(file: UploadFile = File(...)),没有 Request 参数。而 permission_required 装饰器(apps/middleware/permission_required.py:11-19)靠 _extract_request() - 从参数里找 Request,找不到就传 None 进 _check(None) → get_user_id(None) → None.state 抛 AttributeError → 所有用户(含 admin)上传图片/文件都报 500。 - - ▎ 修复:三个函数都补上 request: Request 参数。 - - 2. Excel 导入功能必崩 - - apps/services/level.py:190:result = await upload.upload_file(...) 返回的是 JSONResponse(R.ok/R.failed),不是 dict,result.get('code') 抛 AttributeError → 500。职级导入完全不可用(全库仅此一处)。 - - ▎ 修复:改为 result.status_code == 200 后 json.loads(result.body),或让 upload.upload_file 返回 dict。 - - 3. 权限字符串系统性不匹配 → 非 admin 用户全部被拒(RBAC 失效) - - - 视图层要求:sys:{module}:page(分页)、sys:{module}:batch_delete(批量删除)——见 apps/views/level.py:19/96 及全部模块。 - - 权限节点生成器 apps/services/menu.py:244/274 生成的是 sys:{module}:list 和 sys:{module}:dall。 - - 种子数据 document/djangoadmin.fastapi.elevue.sql 存的也是 sys:level:list / sys:level:dall。 - - 只有 dept 和 menu 视图用 list(与种子一致),其余 20+ 模块全不一致。 - - 结果:非 admin 用户即使在菜单里勾选了"查询",拿到的是 sys:level:list,但 /level/page 端点校验 sys:level:page → 永远"权限不足"。批量删除节点(dall)更是完全无效。 - - ▎ 修复:统一三处(视图、set_permission、种子 SQL)的权限字符串,建议全部收敛为 page / batch_delete。 - - --- - 二、高危(安全隐患 / 影响稳定性) - - 4. 操作日志中间件同步 HTTP 调用阻塞事件循环 - - apps/middleware/operation_log.py:74-91 在 finally 里直接(未走 to_thread)调用 record_operation_log → utils/ip.py:59 的 get_ip_location 用同步 urllib.request.urlopen(..., timeout=3) 访问 - ipapi.co。每次写操作都可能把整个事件循环卡住最长 3 秒,并发下是稳定性隐患。(login_log 在登录服务里已用 asyncio.to_thread 包装,唯独 operation_log 中间件漏了。) - - ▎ 修复:把 record_operation_log 包进 asyncio.to_thread;或 IP 定位改为异步/本地库。 - - 5. 分页参数非数字 → 500(全模块) - - 所有 services 都是 int(request.query_params.get("pageNo", 1)),传 ?pageNo=abc 即抛 ValueError。apps/__init__.py 全局异常只处理了 Authorization/Business/RequestValidation 三类,未捕获 ValueError → 裸 500。22 - 个模块的列表页全中招。 - - 6. 省市区字段 NOT NULL 冲突 → IntegrityError - - apps/forms/member.py:16-18、apps/forms/user.py:20-22 的 province_code/city_code/district_code 是 Optional(None),但模型 apps/models/member.py:66-70、user.py:74-78 是 nullable=False。当 city 数组不足 3 个元素时,字段保持 None → - 插入/更新抛 IntegrityError → 500。add_user/add_member/update_* 均受影响。 - - 7. 上传大小校验在整文件读入内存之后 - - apps/services/upload.py:204-208 先 await file.read() 全量读入内存,再比对 UPLOAD_MAX_SIZE_MB。攻击者传超大文件即可耗尽内存(DoS)。应先用 Content-Length 头预检,或流式分段读取。 - - 8. 存储型 XSS 风险 - - - .svg 在允许列表中,_sanitize_svg 的 apps/services/upload.py:101-128 正则可被绕过(HTML 实体、foreignObject、命名空间等),且无 X-Content-Type-Options: nosniff 保障(若 Nginx 直接服务 uploads/ 目录则风险实锤)。 - - apps/services/notice.py:91/122 富文本 content 只转义 title,正文 HTML 原样存储并原样返回(get_notice_detail 第 76 行),无服务端清洗。公告若展示给前台用户即为存储型 XSS。 - - 9. 密钥/配置管理 - - - .env 未列入 .gitignore(当前项目是 SVN 且 .env 在根目录),内含 DB 密码 root、Redis 密码 123456、JWT 密钥。若入库即泄露。 - - .env 中 FASTAPI_UPLOAD_DIR=E:\DjangoAdmin\... 是写死的异机绝对路径(与当前目录 D:\Temp\... 不符);FASTAPI_IMAGE_URL='http://images.fastapi.elevue' 是假域名。FASTAPI_DEBUG=True、DB_DEBUG=True 默认开。 - - 本应用没有挂载静态目录(全库无 StaticFiles/mount),上传文件必须靠外部 Nginx 服务,否则返回的 fileUrl 不可访问——部署时需确认。 - - --- - 三、中低危 / 优化点 - - 事务与数据一致性 - - apps/services/job.py:216/239 在请求内直接 db.commit(),与 db_session_middleware 统一提交重复(冗余);且 pause_job_by_id 里 job.update_user = job.create_user(应为当前操作人 get_user_id(request))。 - - update_user_info(apps/services/user.py:312-329)bulk update 不触发 onupdate,update_time/update_user 不会被更新。 - - 性能 - - 权限 JOIN(menu.get_permissions_list_sync,apps/services/menu.py:332-345)对 user_role.user_id/role_id、role_menu.menu_id/role_id、menu.parent_id、user.dept_id 等列全无索引,非 admin 每次请求全表扫描。建议加索引。 - - perm_cache.invalidate_role_perms(utils/perm_cache.py:117-118)在异步上下文里直接同步 db.query,未走 to_thread。 - - 22 个模块的 get_*_page 分页/回退逻辑高度重复,可抽取公共分页组件。 - - 健壮性 - - get_access_token(utils/jwt.py:422)replace('Bearer ', '') 大小写敏感,客户端发 bearer 小写会解析失败。 - - captcha 防刷 redis.get 后 incr 存在 TOCTOU 竞态(utils/captcha.py:106-130),并发可绕过上限。 - - detail 路由 id 为裸字符串(apps/views/level.py:28),未做 int 校验,非法值直接进 SQL。 - - base_model.batch_delete_r(apps/models/base_model.py:176)只要有一个 id 不存在就整体报"记录不存在"(all-or-nothing),行为需与前端确认。 - - rich_text.save_file(utils/rich_text.py:58-69)对 URL 中的 .. 未规范化,存在目录穿越到上传目录外的可能(需源文件已存在,低危)。 - - 一致性/代码质量 - - JobLog 用 del_flag(apps/models/job_log.py:46),其余模型统一 is_delete;create_user/update_user 用 String(50) 而非 Integer。 - - forms/dept.py 校验 type 仅 1-2,而模型注释是 1-4。 - - config/sms.py 读的 SMS_URL/SMS_API_KEY... 与 .env 中实际变量(SMS_ALIYUN_*)不一致(当前无用,属预留模块)。 - - R.ok 允许 kwargs 覆盖 code,可能产生 {"ok": True, "code": 1} 的矛盾响应;R.failed 默认 code=1 与中间件"code==0 即成功"判断需保持口径一致。 - - 无测试套件、无 lint/format 配置(CLAUDE.md 已注明),建议至少补 pytest 覆盖鉴权中间件与 CRUD 主路径。 - - --- - 四、建议修复优先级 - - 1. 立即:#1 上传 500、#2 Excel 导入 500、#3 权限字符串统一(功能不可用 / RBAC 失效) - 2. 尽快:#4 事件循环阻塞、#5 分页 500、#6 NOT NULL 冲突、#7 上传内存 DoS - 3. 安全:#8 富文本/SVG XSS、#9 密钥与 DEBUG 配置 - 4. 后续:#11 索引、#12 公共分页组件、其余一致性项 - - 需要我直接开始修复吗?可以按优先级逐个处理,或先修最严重的 #1–#3(三个都是几行的改动,我可以一次性完成并自测)。 \ No newline at end of file diff --git a/modules/cms/ad/service.py b/modules/cms/ad/service.py index 1f1c1e7..42367f1 100644 --- a/modules/cms/ad/service.py +++ b/modules/cms/ad/service.py @@ -92,12 +92,10 @@ def get_ad_detail(ad_id): # 添加广告 def add_ad(request, data: AdForm): - # 广告封面 - cover = data.cover + # 广告封面(cover 列 NOT NULL,非图片广告用空字符串占位) + cover = data.cover or "" if data.type == 1 and cover: cover = save_file(cover, "ad") - else: - cover = None # # 广告标题 # title = data.title @@ -124,12 +122,10 @@ def update_ad(request, data: AdForm): if not ad: return R.failed("记录不存在") - # 广告封面 - cover = data.cover + # 广告封面(cover 列 NOT NULL,非图片广告用空字符串占位) + cover = data.cover or "" if data.type == 1 and cover: cover = save_file(cover, "ad") - else: - cover = None # # 广告标题 # title = data.title