20 KiB
三端问题检测报告
检测对象:
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=Truelogs/tasks.py— Count(filter=Q(is_error=True))api/views/info/baidu_fanyi_appid.py— 密钥改环境变量,endpoint 升 httpstemplates/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_blacklistapp —— 轮换/黑名单双死配置,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)
🔴 严重
- 百度翻译 API 密钥硬编码入库:
api/views/info/baidu_fanyi_appid.py:1-2(appid/appkey 明文),被BaiduFanyiView.py:21、api/tasks.py:19import;endpoint 还是明文http://(:4) - 滑块验证码答案明文下发:
utils/slider_captcha.py:111-117返回体含x_position/y_position,脚本读 JSON 即 100% 通过 - 滑块验证失败不销毁 key:
slider_captcha.py:120-133只有成功才删 key;x 取值空间仅 200、容差 ±5 → 单 key 平均 19 次重试即过,与上条叠加 = 防爆破链路完全失效 - 双登录端点,无保护的绕过有保护的:
/user/user-login/(user.py:652-727)无失败计数无验证码,攻击者绕过带滑块的/user/login/直接无限爆破密码 - 限流标识可伪造:
user.py:733-737取X-Forwarded-For第一段做限流 key,每次伪造不同 XFF 即绕过失败计数(utils/captcha.py:88-102) - JWT 三重配置问题:access 7 天过长 + 轮换/黑名单互斥死配置 + blacklist app 未注册(settings.py:113-116,135-169)
- 每日统计任务必然崩溃(已实测复现):
logs/tasks.py:46Count('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) - 扫码登录:
QRStatusViewAllowAny 轮询即拿完整 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)
🔴 严重
- 存储型 XSS(文章正文):
ArticleDetail.tsx:412dangerouslySetInnerHTML渲染用户文章,唯一防线htmlSanitizer.ts4 条正则可绕过——EVENT_HANDLERS(:6)要求引号,<img src=x onerror=alert(1)>穿透;DANGEROUS_TAGS(:5)缺 svg/math/style;:3 注释自认该换 DOMPurify。链路:ArticleEditor contentEditable(:81,1122) → 后端 → 详情页 - JWT 存 localStorage(与 XSS 叠加=完整身份窃取链):axios.ts:33-49、authSlice.tsx:83-123、LoginModalContext.tsx:94
- 守卫形同虚设: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)
🔴 严重
- 硬编码内网 HTTP 地址:
RetrofitClient.kt:15http://192.168.5.7:8000/,换环境/发版必失效;AndroidManifest.xml:21全局usesCleartextTraffic="true"无 network_security_config → JWT 明文上网 - BODY 级日志无条件开启:RetrofitClient.kt:17-19 无
BuildConfig.DEBUG判断,release 包把密码/验证码/JWT 全打进 logcat - 4 个 API 路径与后端不一致必 404(见跨端问题 1)
🟡 中等
- runBlocking 读 DataStore 且在 Compose 组合中直接调用(TokenManager.kt:18,39 + MessagesScreen.kt:36、ProfileScreen.kt:41)——每次重组同步磁盘 IO
MainScope().launch无管理协程 6 处(MessagesScreen 等),永不 cancel、失败也清 UIRetrofitClient.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(立即):
- 轮换百度翻译 appkey(已泄露入库),迁环境变量 + 改 https
- 封死
/user/user-login/无保护登录或补齐其失败计数/验证码;滑块响应去掉 x_position、失败即删 key - 换 htmlSanitizer 为 DOMPurify;管理路由(App.tsx:181-186)包 AuthGuard
- Android:修 4 个 404 路径、BASE_URL 走 BuildConfig、日志拦截器加 DEBUG 判断
- 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 官方兼容性