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

235 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 三端问题检测报告
> 检测对象:`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 官方兼容性