Files
vscode-workbench/三端问题检测报告.md
T

20 KiB
Raw Blame History

三端问题检测报告

检测对象:chunyu_project(Django 后端)、chunyu_project_react(React Web)、android-chunyu(Android) 检测方式:3 个并行深度审计(后端/前端/Android)+ 独立交叉验证(所有 🔴 级发现均经第二人核实源码,部分经运行时复现) 日期:2026-09-06


⭐ 修复状态更新(2026-09-06 第二轮)

以下 P0 问题已在本轮修复(修改清单见文末「已执行修复」章节):

问题 状态 修复方式
13 个 API 断链(Android 4 + Web 9) ✅ 已修复 Web request.ts 9 处 /users/→/user/;Android 4 路径改指真实端点并补齐请求体字段;后端新增统一 user/favorites/toggle/ 端点
滑块验证码防爆破失效 ✅ 已修复 后端不再下发 x_position;验证成败均销毁 key;x 上限收到可拖动范围;React 组件新增服务端 1:1 坐标渲染模式
/user/user-login/ 无防护 ✅ 已修复 补齐失败计数 + 滑块验证码门控;双端点限流标识改用 X-Real-IP+账号(不可伪造)
存储型 XSS ✅ 已修复 htmlSanitizer.ts 重写为 DOMPurify 白名单模式(新增 dompurify 依赖)
百度密钥硬编码 ✅ 已修复(需人工轮换) 改为 BAIDU_FANYI_APPID/BAIDU_FANYI_APPKEY 环境变量注入,endpoint 升级 https;旧密钥已泄露,需在百度控制台轮换
refresh token 三端死代码 ✅ 已修复 后端挂载 /user/token/refresh/ + 注册 token_blacklist + 开启轮换;Web axios 401 自动刷新重试(单飞锁);Android OkHttp Authenticator 同机制
logs/tasks.py 每日统计崩溃 ✅ 已修复 Count(filter=Q(is_error=True)),编译验证通过

修复过程中新发现并一并修复的 🔴:ChangeEmailAPIView/ChangePhoneAPIView 此前从未调用 avalidate(),验证码校验为死代码(任何已登录用户可无码改绑邮箱/手机号)——已补上校验调用。

验证结果:后端 9 个改动文件 py_compile/AST 全过 + manage.py check 0 错误 + 路由可解析 + blacklist 模型可用 + 滑块验证码行为经 4 项运行时测试全过(旧 dict 写法确认编译失败、新 Q 写法编译通过);前端 tsc 0 错误 + pnpm run build(tsc+vite)exit 0 + eslint 相对基线无新增错误;Android 无 Gradle 环境(无 wrapper、本机无 gradle/SDK platforms),改动经逐文件精读复查,建议在 Android Studio 中编译验证。

部署注意事项:① 后端需执行 migrate(token_blacklist 表);② 设置 BAIDU_FANYI_APPID/BAIDU_FANYI_APPKEY 环境变量;③ Android 需在 IDE 中编译并安装新包。

已执行修复 · 文件清单

后端 chunyu_project(10 文件):

  • utils/slider_captcha.py — 响应去除 x_position;验证成败均销毁 key;x 上限 239(可拖动范围)
  • user/views/user.py — UserLoginAPIView 补失败计数+滑块门控;双登录端点标识改 X-Real-IP+账号
  • user/views/email.py / user/views/phone.py — 补调 avalidate 验证码校验(新发现 🔴)
  • user/views/favorites.py — 新增 FavoriteToggleView(tool/article/course/api 统一切换)
  • user/urls.py — 新增 favorites/toggle/、token/refresh/ 路由
  • chunyu_project/settings.py — 注册 token_blacklist;ROTATE_REFRESH_TOKENS=True
  • logs/tasks.py — Count(filter=Q(is_error=True))
  • api/views/info/baidu_fanyi_appid.py — 密钥改环境变量,endpoint 升 https
  • templates/test_slider_captcha.html — 提示文案同步(该页本就不依赖下发答案)

前端 chunyu_project_react(6 文件 + lockfile):

  • src/utils/request.ts — 9 处 /users/→/user/(git diff 逐行确认)
  • src/utils/axios.ts — 401 自动刷新(single-flight)+ 重放原请求;auth 端点豁免
  • src/utils/htmlSanitizer.ts — 重写为 DOMPurify 白名单消毒(新增 dompurify 3.4.14)
  • src/components/SliderCaptcha/SliderCaptcha.tsx + captchaUtils.ts — 服务端模式:1:1 坐标渲染后端图/拼图块,不再需要下发答案
  • package.json / pnpm-lock.yaml — 新增 dompurify 依赖

Android android-chunyu(4 文件):

  • data/api/UserApi.kt — bind-email→change-email、bind-phone→change-phone、user/{id}→public-profile/{id};请求体字段对齐(new_email/new_phone)
  • data/repository/UserRepository.kt — 传参改为命名参数对齐新字段
  • data/api/RetrofitClient.kt — OkHttp Authenticator 401 自动刷新重放;release 关闭 BODY 日志
  • data/local/TokenManager.kt — 新增 getRefreshToken/saveAccessOnly

总览

端 🔴 严重 🟡 中等 🟢 低 权威命令结果
后端 Django 7 20 9 277 个 .py 全部通过 py_compile;makemigrations --check 检出 4 处迁移漂移;Count(filter=dict) 运行时 bug 实测复现
前端 React 3 10 7 tsc -b --force 0 错误;eslint . 491 errors + 63 warnings
Android 4 9 6 未跑 gradle build;仓库仅 93 个 tracked 文件且全为源码(卫生良好),缺 Gradle Wrapper
跨端断裂 2 类 — — 13 个 API 端点必 404(Android 4 + Web 9);refresh token 三端全链路死代码

一、跨端断裂问题(多端联合实锤,优先修复)

🔴 1. 13 个 API 端点前后端路径不一致,调用必 404

Android → 后端(4 个),逐一比对过后端各 urls.py:

Android 声明 位置 后端实际 影响
POST user/bind-email/ UserApi.kt:78 无此路由(只有 send-email-code/+change-email/) 邮箱绑定必失败(ProfileEmailScreen.kt:122)
POST user/bind-phone/ UserApi.kt:82 无此路由(只有 send-phone-code/+change-phone/) 手机绑定必失败(ProfilePhoneScreen.kt:112)
GET user/{id}/ UserApi.kt:87 公开资料实为 user/public-profile/<int:user_id>/ 用户主页必 404(AppNavHost.kt:77,192)
POST user/favorites/toggle/ FavoritesApi.kt:33 后端只有 user/favorites/,toggle 分散在各业务前缀下 收藏切换必 404(FavoritesRepository.kt:22)

Web → 后端(9 个):request.ts:605-640 的 activities / uploadCover / privacy(GET/PUT) / notification-settings(GET/PUT) / devices / removeDevice / clearOtherDevices 全部用 /users/(复数) 前缀,而后端全库只有 user/ 单数路由(根 urls.py:67)。

  • 开发环境:vite 代理 /user 前缀恰好也匹配 /users/* → 转给后端 → 404
  • 生产环境:nginx 正则 ^/(...|user|...)/ 要求斜杠,/users/ 不匹配 → 回退 SPA 返回 HTML
  • 两个环境都必失败。其余 43+ 个端点核对过均一致。

🔴 2. Refresh Token 三端全链路死代码

  • 后端 grep 全库:无任何 TokenRefresh/TokenObtainPair 视图或路由(rest_framework_simplejwt.urls 从未挂载),但登录在 4 处下发 refresh(user/views/user.py:184,224,690,806)
  • settings.py:REFRESH_TOKEN_LIFETIME=30天 + ROTATE_REFRESH_TOKENS: False + BLACKLIST_AFTER_ROTATION: True + 未注册 rest_framework_simplejwt.token_blacklist app —— 轮换/黑名单双死配置,token 泄露 7 天内无法撤销
  • Web:refresh_token 存 localStorage,全库 0 处刷新调用,401 直接硬登出(axios.ts:108-113)
  • Android:TokenManager 存 REFRESH_TOKEN,无刷新接口无 401 重试(TokenManager.kt:14,27)
  • 结论:30 天长效凭据被三端持久化,却永远无法使用、无法轮换;要么补 /user/token/refresh/,要么停止签发

二、后端问题(chunyu_project)

🔴 严重

  1. 百度翻译 API 密钥硬编码入库:api/views/info/baidu_fanyi_appid.py:1-2(appid/appkey 明文),被 BaiduFanyiView.py:21、api/tasks.py:19 import;endpoint 还是明文 http://(:4)
  2. 滑块验证码答案明文下发:utils/slider_captcha.py:111-117 返回体含 x_position/y_position,脚本读 JSON 即 100% 通过
  3. 滑块验证失败不销毁 key:slider_captcha.py:120-133 只有成功才删 key;x 取值空间仅 200、容差 ±5 → 单 key 平均 19 次重试即过,与上条叠加 = 防爆破链路完全失效
  4. 双登录端点,无保护的绕过有保护的:/user/user-login/(user.py:652-727)无失败计数无验证码,攻击者绕过带滑块的 /user/login/ 直接无限爆破密码
  5. 限流标识可伪造:user.py:733-737 取 X-Forwarded-For 第一段做限流 key,每次伪造不同 XFF 即绕过失败计数(utils/captcha.py:88-102)
  6. JWT 三重配置问题:access 7 天过长 + 轮换/黑名单互斥死配置 + blacklist app 未注册(settings.py:113-116,135-169)
  7. 每日统计任务必然崩溃(已实测复现):logs/tasks.py:46 Count('id', filter={'is_error': True}) 传 dict 应传 Q 对象,Django 5.2 实测抛 AttributeError → beat 任务 generate_daily_stats 每天执行必失败

🟡 中等(摘要,完整清单见子审计记录)

  • SECRET_KEY 公开兜底值 + DEBUG 默认 True(settings.py:24-27)——生产漏配即弱密钥+debug 全开,建议 fail-fast
  • MySQL 分支是定时炸弹:视图大量用异步 ORM(aget/asave),MySQL 后端不支持,切 DB_ENGINE=mysql 即抛 SynchronousOnlyOperation(settings.py:184-216)
  • 4 处迁移漂移(makemigrations --check 实测):history 的 TYPE_CHOICES 加了 api/utility 但迁移未生成(history/models.py:5-11 vs 0002_add_learn_type.py:16)
  • 扫码登录:QRStatusView AllowAny 轮询即拿完整 JWT(qr_login.py:58-65);QRStatusConsumer 的实时推送是死功能——全项目无任何 group_send 发往 qr 组,前端只能轮询
  • handle_typing/handle_read_receipt 不校验会话成员(chat/consumers.py:86-120 vs save_message:177 的校验),任意连接者可向任意会话广播 typing 指示器
  • "清空会话"物理删除双方全部消息(chat/views.py:447-459),与 swagger 描述矛盾
  • ConversationClearView 类体内混入"标记已读"方法的复制粘贴事故,REST 已读接口实际缺失(chat/views.py:460-478 + chat/urls.py:16 注释路由)
  • FileUploadView 无类型/大小限制(chat/views.py:546-564)→ 存储 DoS/恶意文件托管
  • SSRF:FavoriteStickerFromMessageView 对消息内容中任意 http URL 发起服务端 GET,无内网黑名单(chat/views.py:692-713),可探测 169.254.169.254 等
  • 翻译内容进 URL:requests.post(url, params=payload) 应为 data=(api/tasks.py:38,65);Celery 调用不在 try 内,broker 挂了把成功翻译打成 500(BaiduFanyiView.py:105-116)
  • 邮箱验证码:错误不计数、成功不清除可复用(user/views/user.py:151-250);换绑验证码 6 位纯数字无失败限制(user_serializers.py:267,421)
  • 注册生成随机密码但从不告知用户(user_serializers.py:62-78)
  • 用户可控 ordering 未白名单(article/views.py:80-81、apidirectory/views.py:62-68)→ FieldError 500 / 排序 oracle;对照 learn/views.py:51-67 有正确白名单
  • CDNStaticFileView AllowAny 不校验 published/is_free,未发布章节可匿名读(learn/views.py:507-541)
  • 短链 AllowAny 无限流 + 任意 URL 重定向 → 钓鱼跳转服务(shorturl/views.py:39-105,171)
  • ?page=abc 直接 500 的复制粘贴模式(shorturl:186、search:49、chat:365、wallet:68)
  • tool 路由双重挂载:urls.py:77(tool/)与 api/urls.py:31(api/tool/)同一份 tool.urls(Android 端用的正是后一份,勿直接删)
  • 每请求新建 daemon 线程写日志库(logs/middleware.py:60-69)+ query_string 未脱敏(qr token 明文入库:113)
  • 依赖矛盾:aioredis(零引用)/redis 6.4 并存、psycopg2/psycopg3 并存、mysqlclient 与异步架构矛盾、gunicorn/granian 并存、simplejwt 5.2.2(2022) 对 Django 5.2 兼容性待确认、django_celery_results 注册但 RESULT_BACKEND 用 Redis、public_profile analytics 全 0 硬编码(user.py:888-899)
  • 31 处宽泛 except Exception:(业务视图含 user.py:882/997、learn:537;真裸 except: 仅 test_all_apis.py:17)

🟢 低

  • 21 个一次性脚本散落根目录(含硬编码 admin123456/test1234);2 个 SQLite 入库(实测 0 行无泄露,schema 暴露);53 个 __pycache__ 入库(310/312 双版本混杂);user.py.bak;三套 requirements 并存
  • python-dotenv 装了但从未 load_dotenv,环境变量机制实际不可用
  • 点赞无事务无唯一约束,并发计数漂移(article/views.py:438-452);ToolCategorySerializer N+1(tool/serializers.py:15-16)
  • cache IGNORE_EXCEPTIONS: True → Redis 挂时验证码触发判定静默放行(settings.py:248 + utils/captcha.py:88-93)
  • swagger/redoc public=True 全量暴露 schema;CORS_ALLOW_CREDENTIALS 与纯 Bearer 组合无必要

三、前端问题(chunyu_project_react)

🔴 严重

  1. 存储型 XSS(文章正文):ArticleDetail.tsx:412 dangerouslySetInnerHTML 渲染用户文章,唯一防线 htmlSanitizer.ts 4 条正则可绕过——EVENT_HANDLERS(:6)要求引号,<img src=x onerror=alert(1)> 穿透;DANGEROUS_TAGS(:5)缺 svg/math/style;:3 注释自认该换 DOMPurify。链路:ArticleEditor contentEditable(:81,1122) → 后端 → 详情页
  2. JWT 存 localStorage(与 XSS 叠加=完整身份窃取链):axios.ts:33-49、authSlice.tsx:83-123、LoginModalContext.tsx:94
  3. 守卫形同虚设:MobileGuard 无条件渲染 children(MobileGuard.tsx:63-115);/console、/article-manage、/bug-detail、/article-editor、/learn-manage、/learn-editor 六个管理路由零守卫裸奔(App.tsx:181-186,对比 :154/:167 有真守卫)

🟡 中等

  • 依赖冗余(import 实测):zustand 0 引用、react-lorem-ipsum 0 引用、components/ui 整目录 0 引用(Chakra 全链死代码)、Markdown.tsx 0 引用(react-syntax-highlighter 死代码)、qrcode 双库并存、@types/qrcode 误放 dependencies
  • 三锁文件并存且全被 git 追踪(package-lock 369KB + pnpm-lock 210KB + yarn.lock 193KB),packageManager 声明 pnpm@11
  • ApiDocs.tsx:64 硬编码 http://localhost:8000/swagger.json(后端实际 8002),生产必挂
  • WebSocket token 走 URL 查询串(request.ts:491-495)→ 进代理日志
  • 无 token 刷新机制(见跨端问题 2)
  • i18n 6 语言 JSON 约 694KB 静态打进首屏 bundle(locales/index.ts:1-6)
  • locales 目录 24 个 .cjs 补丁脚本 + zh.json.bak 被 git 追踪,翻译靠脚本打补丁
  • Playwright baseURL 5175 vs dev 端口 5173,默认跑不起来(playwright.config.ts:8)
  • 桌面/移动双实现 28 对约 1.6 万行,多对行数几乎相同(TextDiff 212/211 等),复制粘贴平行维护

🟢 低

  • 28 张根目录 PNG、24 个 .cjs 临时脚本、6 个 Python 文件混在前端仓库、tsconfig.tsbuildinfo 入库、README 还是 Vite 模板
  • console.* 残留 51 处;TODO/FIXME 实际 0(粗筛 9 处均为误报已核实)
  • 超大文件:Chat.tsx 1243 行、ArticleEditor.tsx 1228 行
  • SliderCaptchaDemo 死页面(228 行无路由);测试仅 2 个 spec 对 65 个页面
  • vite /s 代理靠 bypass hack;allowedHosts 硬编码内网 IP

四、Android 问题(android-chunyu)

🔴 严重

  1. 硬编码内网 HTTP 地址:RetrofitClient.kt:15 http://192.168.5.7:8000/,换环境/发版必失效;AndroidManifest.xml:21 全局 usesCleartextTraffic="true" 无 network_security_config → JWT 明文上网
  2. BODY 级日志无条件开启:RetrofitClient.kt:17-19 无 BuildConfig.DEBUG 判断,release 包把密码/验证码/JWT 全打进 logcat
  3. 4 个 API 路径与后端不一致必 404(见跨端问题 1)

🟡 中等

  • runBlocking 读 DataStore 且在 Compose 组合中直接调用(TokenManager.kt:18,39 + MessagesScreen.kt:36、ProfileScreen.kt:41)——每次重组同步磁盘 IO
  • MainScope().launch 无管理协程 6 处(MessagesScreen 等),永不 cancel、失败也清 UI
  • RetrofitClient.create() 每次新建 Retrofit+OkHttpClient,全项目 29 个调用点,连接池零复用
  • Token 明文存 DataStore + allowBackup=true + 空 backup_rules → 云备份带出凭据
  • 扫码页相机/MLKit/executor 不释放(ScanLoginScreen.kt:126-165,无 DisposableEffect)
  • UI 层 is Resource.Error -> {} 空分支吞错,空态与失败不可区分;11 个 repository 宽泛 catch
  • release 未混淆(isMinifyEnabled=false)+ 无 signingConfigs,出不了正式签名包
  • 钱包模块全假数据(PointsScreen:27-28 等);Chat 发送按钮只清空输入框(后端无发送接口被调用);忘记密码纯 UI 无网络调用
  • 缺 Gradle Wrapper,Gradle 版本未锁定

🟢 低

  • 依赖偏旧(Compose BOM 2024.02、Retrofit 2.9、Coil 2.5);kotlinCompilerExtension 1.5.8↔Kotlin 1.9.22 是官方匹配组合,无错配
  • refresh token 存而不用;Gson LOWER_CASE_WITH_UNDERSCORES 与 camelCase 模型混用靠约定维持
  • MessageApi markAsRead 用 String id 对后端 int pk(待确认)
  • ✅ 正面:权限最小化(仅 3 项);DataStore 而非明文 SharedPreferences;仓库无构建产物入库

五、部署层问题(Docker / nginx,独立核实)

  • 🟡 前端 Dockerfile 弃用 pnpm:chunyu_project_react/Dockerfile:9 用 npm install,与 pnpm-lock.yaml 漂移,构建不可复现;且 npm run build 含 tsc -b,类型错误会直接炸镜像(本次实测 tsc 0 错,暂时安全)
  • 🟡 后端容器启动自动 migrate:chunyu_project/Dockerfile:27 多副本竞态反模式
  • 🟡 nginx 与 vite 代理漂移:nginx.conf:28 正则缺 /users(致 Web 9 断链在生产返回 HTML)、缺 /swagger.json;/i18n 两边都有但前端代码 0 调用(过时规则,🟢)
  • 🟢 后端镜像打包 21 个开发脚本(.dockerignore 未排除);Python 本地 3.10 vs Docker 3.12 漂移
  • ✅ 两份 .dockerignore 本身配置良好(db.sqlite3/media/node_modules 已排除)

六、修复优先级建议

P0(立即):

  1. 轮换百度翻译 appkey(已泄露入库),迁环境变量 + 改 https
  2. 封死 /user/user-login/ 无保护登录或补齐其失败计数/验证码;滑块响应去掉 x_position、失败即删 key
  3. 换 htmlSanitizer 为 DOMPurify;管理路由(App.tsx:181-186)包 AuthGuard
  4. Android:修 4 个 404 路径、BASE_URL 走 BuildConfig、日志拦截器加 DEBUG 判断
  5. Web:/users/ → /user/(request.ts 9 处)

P1(本周): 6. 补 /user/token/refresh/ + 注册 blacklist app + ROTATE 改 True;或全端停止签发 refresh 7. 修 logs/tasks.py:46 的 Count(filter=Q(...));补 4 处迁移 8. SSRF(chat/views.py:692)加 URL 白名单/内网黑名单 9. Android RetrofitClient 单例化 + 摘除 runBlocking

P2(迭代内): 10. 统一锁文件(删 package-lock/yarn.lock)、清理死依赖死代码(zustand/Chakra 链/Markdown.tsx)、前端 Dockerfile 改 pnpm --frozen-lockfile 11. 后端验证码类接口统一加失败计数;shorturl 加限流


七、检测方法与覆盖说明

  • 3 个并行子代理深度审计 + 协调者独立验证:所有跨端结论(13 断链、refresh 死链、XSS 链、Docker/nginx)均经协调者对照源文件二次核实;后端 Count(filter=dict) bug 经独立脚本在 Django 5.2 实测复现;makemigrations --check 为实际运行输出
  • 已核实正面项(非问题):后端 277 文件无语法错误;Celery beat 注册的 4 个 cleanup 任务存在且签名匹配;ASGI 引用的 consumers/routing 均存在;Android Kotlin/Compose 编译器版本匹配;Android 仓库卫生良好;两份 .dockerignore 配置正确
  • 已知未覆盖:65 个前端页面的逐一业务审查、后端 weather/air_quality/bug/serializers 等部分模块全文、Android 未运行 gradle build、simplejwt 5.2.2 与 Django 5.2 官方兼容性