175 lines
15 KiB
Markdown
175 lines
15 KiB
Markdown
# RainWeb 优化重构实施计划(P0 / P1 / P2)
|
||
|
||
> 依据:数据库优化师(better-sqlite3 选型调研)、后端架构师(架构评审)、应用安全工程师(安全审计)、前端路由专项深挖 四份报告合并。
|
||
> 数据库路线已定:**better-sqlite3 + Node 22 LTS**(放弃 sql.js 与 PostgreSQL)。
|
||
> 前端路由:用户确认问题很大 → 按「方案 A 止血 → 方案 B 协议收编」两步走(见 P1-14 / P2-19)。
|
||
> 本计划仅执行顺序与细节,**未开始任何代码改动**。
|
||
|
||
## 全局前提(每阶段执行前)
|
||
|
||
1. 备份:`cp -r data data.bak-<日期>`(`data/rainweb.db` 是唯一数据文件)
|
||
2. 每阶段一个 git commit,commit message 中文,如 `P0: 删除 Web 更新链路,修复鉴权漏洞`
|
||
3. 无测试环境,验证方式 = curl 接口 + 浏览器刷新 + `node cli.js` 命令
|
||
|
||
---
|
||
|
||
## P0 紧急止损(独立项,可并行,约半天)
|
||
|
||
### P0-1 删除 Web 更新链路(消除 R6 + .git 覆盖风险)
|
||
- `server.js`:删除 84-165 行 `/api/update/check` + `/api/update/run`,保留 `/api/version`(81-82 行)
|
||
- `public/js/admin.js`:删除 checkUpdate/runUpdate 函数及更新按钮 UI(约 692-731 行区域)
|
||
- `cli.js`:`cmdUpgrade` 保留 git pull 分支(走本地 Gitea origin),删除非 git 仓库的 zip 下载分支(266-346 行区域)
|
||
- 理由:用户即上游,zip 覆盖式更新是错误范式;`/api/update/run` 无鉴权=RCE 面,且 exclude 未排除 `.git` 会冲掉 git 历史
|
||
- 风险:低。注意前端引用一并删除,避免 404
|
||
- 验证:`curl /api/update/check` → 404;admin 面板打开控制台无报错;`node cli.js upgrade` 走 git pull 分支正常
|
||
|
||
### P0-2 JWT_SECRET 随机化(消除 R4)
|
||
- `middleware/auth.js`:改为 `process.env.JWT_SECRET` 必须存在(长度 ≥32),否则启动失败;删除硬编码回退
|
||
- `server.js` `start()`:首次启动若无 `JWT_SECRET`,用 `crypto.randomBytes(48).toString('hex')` 生成并写入 `.env.json`
|
||
- 风险:低;已签发旧 token 全部失效(自部署可接受)
|
||
- 验证:删 `.env.json` 重启 → 自动生成 secret;登录 → `curl /api/auth/me` 200
|
||
|
||
### P0-3 setup 接管漏洞(消除 R5)
|
||
- `routes/setup.js`:`POST /complete` 加 `authMiddleware, adminOnly`;`GET /setup/status` 响应移除 `default_password` 字段(只保留 `setup_complete`)
|
||
- 风险:低
|
||
- 验证:未登录 `curl -X POST /api/setup/complete` → 401;登录后正常
|
||
|
||
### P0-4 proxy SSRF 最小防护(消除 R3 的 SSRF 部分)
|
||
- `routes/proxy.js`:`GET /fetch` 加 `authMiddleware, adminOnly`;`new URL(target)` 后校验 hostname 非 `127.0.0.1`/`::1`/`localhost`/`10.`/`192.168.`/`172.16-31.`/`169.254.`/`fc00:`/`fe80:`
|
||
- 风险:低(本功能只服务于 admin_links 面板嵌入)
|
||
- 验证:未登录请求 → 401;`/api/proxy/fetch?url=http://127.0.0.1:3001/` → 拒绝
|
||
|
||
### P0-5 邮件验证码回显与滥用(消除 R7)
|
||
- `routes/email.js`:`send-verify` 响应删除 `code` 回显;验证码生成改 `crypto.randomInt(10000000, 100000000)`;补回"删除 pending 后重建"的 bug(或废弃该接口由 register 内联重发)
|
||
- 风险:低
|
||
- 验证:调 send-verify → 响应无 code 字段;邮件仍能收到验证码
|
||
|
||
---
|
||
|
||
## P1 安全加固 + 数据层统一(依赖顺序:11 → 12,其余独立)
|
||
|
||
### P1-6 上传白名单 + /uploads 防护头(消除 R1)
|
||
- `routes/upload.js`:`shaFileUpload` 内校验 `path.extname(...).toLowerCase()` 在 `ALLOWED_EXT = ['.png','.jpg','.jpeg','.gif','.webp','.pdf','.zip','.txt','.md']` 内,否则报"不支持的文件类型";图片额外校验 `file.mimetype` 以 `image/` 开头
|
||
- `server.js:63`:`/uploads` 静态服务加 `X-Content-Type-Options: nosniff` + `Content-Security-Policy: sandbox; default-src 'none'`;非白名单扩展名强制 `Content-Disposition: attachment`
|
||
- 风险:低;已有历史上传文件不受影响(仅新上传受限)
|
||
- 验证:登录后上传 `x.html` → 400;上传 png → 200;访问 `/uploads/<hash>.png` 响应头含 nosniff
|
||
|
||
### P1-7 内容净化双端统一(消除 R2)
|
||
- `ssr.js`:引入 `dompurify + jsdom`(服务端),`renderSSR` 中 `marked.parse` 结果过 `DOMPurify.sanitize`;`ssrPage` 中 `title/siteName/ogDesc` 全部 HTML 转义,JSON-LD 用 `JSON.stringify` 序列化
|
||
- `public/js/render.js`:引入 DOMPurify(本地化到 public/js/vendor/ 或 CDN + SRI),`marked.parse` 结果同样过 sanitize
|
||
- 风险:中(渲染路径改动,需走查博客/论坛/SEO 页)
|
||
- 验证:发帖内容含 `<img src=x onerror=alert(1)>` → SSR 页与 SPA 页均不弹窗、标签被剥;正常 markdown 渲染不变
|
||
|
||
### P1-8 登录限流 + 服务端验证码(消除 R8)
|
||
- 新增 `express-rate-limit` 依赖:`/api/auth/login` 15 分钟 10 次;`/api/email/send-verify` 60s/次
|
||
- `routes/auth.js`:login/register 按 `captcha_login`/`captcha_register` 设置服务端校验 captcha token(与 captcha.js 的 verify 存储联动,token 与 username 绑定);`routes/forum.js` 发帖同理(`captcha_forum`)
|
||
- 删除 `verifyCaptchaToken` 桩函数(auth.js:39-52)
|
||
- 风险:中(验证码链路前后端要同步改)
|
||
- 验证:连错密码 11 次 → 429;开启 captcha_login 后登录需先过验证码
|
||
|
||
### P1-9 密码箱解锁限流 + KDF 加固(消除 M1)
|
||
- `routes/passwords.js`:`/unlock` 加失败计数(3 次/5 分钟,指数退避);解锁会话滑动过期替换一次性 setTimeout
|
||
- PBKDF2 迭代 100k → 600k(OWASP 2023 建议);注意同步执行阻塞事件循环,可接受(单用户站)
|
||
- 风险:低
|
||
- 验证:连续输错 PIN 3 次 → 锁定提示;正确解锁正常;旧 PIN 密文在 KDF 迭代变更后需重设(set-pin 已存在)
|
||
|
||
### P1-10 鉴权与信息收敛(消除 M2-M7)
|
||
- `middleware/auth.js`:`adminOnly` 增加 `db.get('SELECT role FROM users WHERE id=?')` 复查(M2)
|
||
- `routes/blog.js`:`?all=1` 加 `authMiddleware, adminOnly`;`GET /posts/:id` 未发布仅 admin/作者可见(M3)
|
||
- 错误信息收敛:`routes/forum.js:21,35`、`server.js:163`、`routes/email.js:54,93` 对外统一 `{ error: '操作失败' }`,详情仅 console.error(M4)
|
||
- `routes/email.js:17`、`auth.js:93`、`profile.js:25`、`proxy.js:22` 的 `rejectUnauthorized:false` 收敛为配置项(默认关闭校验仅当 SMTP 自签时开启)(M5)
|
||
- `public/js/passwords.js:113`、`forum.js:76`:内联 onclick 字符串插值改 `data-id` + `addEventListener` 委托(M6)
|
||
- **S3-3 escapeHtml 引号问题**:`escapeHtml` 不转义 `'`/`"`,用于属性/JS 字符串上下文可注入断链(forum.js:76、passwords.js:113、admin.js:74/402-404)——新增 `escapeAttr`(全转义)用于属性上下文,或随 M6 的 data-id 改造一并消除;帖子标题/分类为他人可见,属存储型 XSS 面
|
||
- `routes/profile.js`:avatar 仅接受 `/uploads/avatars/` 站内路径(M7)
|
||
- 风险:中;验证:逐项 curl 测试(未登录 401/403、草稿对匿名 404、报错无堆栈详情)
|
||
|
||
### P1-11 cli.js 统一数据层(ora-2 P1-1)
|
||
- `cli.js`:删除 9-51 行重复封装(DB_PATH/getDb/dbRun/dbGet/dbAll),改为 `const db = require('./db')`;`cmdStatus/cmdPassword/cmdCaptcha/cmdConfig` 改用 `db.get/db.all`;各命令 `await getDb()`
|
||
- `data.db` 旧路径彻底退役(server.js 启动迁移逻辑保留,把旧文件搬进 data/)
|
||
- 风险:中低;验证:备份后依次跑 `node cli.js status/config/password/captcha` 全部成功且落库
|
||
|
||
### P1-12 schema_version 迁移框架(ora-2 P1-2,依赖 P1-11)
|
||
- `db.js`:`initTables()` 收敛——"确保列存在"函数(幂等补列,逻辑同现有 try/catch)+ `PRAGMA user_version` 记录迁移版本;未来迁移写成 `{ version, up() }` 数组顺序执行
|
||
- 风险:中;验证:旧库启动自动补列无报错;连续启动两次幂等
|
||
|
||
### P1-13 deploy 脚本处置(ora-2 P1-3)
|
||
- 删除 `deploy.sh` + `deploy.bat`(指向 GitHub 旧仓库,对自托管用户无意义);README 相关段落同步清理
|
||
- 风险:低;验证:`ls` 确认删除;README 无残留引用
|
||
|
||
### P1-14 前端路由止血(方案 A,约 0.5 天)
|
||
> 专项深挖结论:PJAX 只替换 `<main>`、目标页 `<script>` 从不执行,而 5 个 PJAX 壳页各自加载不同共享脚本子集 → 从某些页面进入时目标页依赖缺失(结构性缺陷 S1-3)。已确认 bug:S1-1 首页空白、S1-2 个人中心空白、S1-3 脚本依赖矩阵、S2-1 监听器重复累积、S2-2 论坛详情后退进 SSR 页、S2-4 滚动不恢复、S2-5 登录态/设置变化后导航不刷新、S2-6 login/register 白跳。
|
||
- **A1 PJAX_PATHS 收敛**:`router.js` 只保留 5 个有 `_pageConfig` 的页面(`/`、`/blog.html`、`/forum.html`、`/admin.html`、`/passwords.html`);移除 `login.html`/`register.html`/`profile.html`(整页导航约定,根治 S1-2、S2-6)
|
||
- **A2 统一共享脚本集**:5 个 PJAX 壳页 html 全部加载完整共享集(marked + render + captcha + music-embed + theme + api + nav + router),消除 S1-3 全部矩阵缺口
|
||
- **A3 首页逻辑外部化**:`public/index.html:54-96` 内联 HOMEPAGE 抽为 `public/js/homepage.js` 并注册进 `_pageConfig`(修 S1-1;顺带合并 S3-4 与 blog.js 的 loadSidebar 重复)
|
||
- **A4 `_loadScript` 判重**:`router.js:91-99` 加已加载检测,修 S2-1 监听器累积
|
||
- **A5 popstate 分支 + 滚动**:popstate 识别 `e.state.forumPostId` 直接还原论坛详情(修 S2-2 不再 reload 进 SSR 页);navigate 时 `scrollTo(0,0)`(修 S2-4)
|
||
- **A6 导航重渲染钩子**:loadPage 完成后调 `NAV.render?.()`(修 S2-5)
|
||
- 风险:低(login/register/profile 当前本就走整页兜底,无回归面);A2 可能触发 S3-2 重复定义,但各文件实现相同无碍
|
||
- 验证:passwords.html 整页进入 → PJAX 依次进博客(markdown 正常)/论坛(发帖弹验证码、详情正常)/首页;论坛详情后退回 SPA 列表无 reload;循环 PJAX 5 次 forum.js 只加载一次
|
||
|
||
---
|
||
|
||
## P2 数据库换轨 + 工程卫生(依赖 P1-11/P1-12)
|
||
|
||
### P2-15 better-sqlite3 迁移(lib-1 调研结论)
|
||
- **前置**:Node 升 22 LTS(宝塔 Node 版本管理器 / nvm);README 与 Docker 示例同步(`node:20` → `node:22`)
|
||
- 依赖:`npm install better-sqlite3`(v13,预编译随包,宝塔 glibc x64 免编译)
|
||
- `db.js`:`initSqlJs()` 异步初始化 → `new Database(DB_PATH)` 同步;`run/get/all` 重写:
|
||
- `run` → `db.prepare(sql).run(...params)` + `info.lastInsertRowid`
|
||
- `get` → `stmt.get(...params)`;`all` → `stmt.all(...params)`
|
||
- 删除 `saveDb()`/`db.export()`(SQLite 事务原生落盘);返回签名不变,40+ 业务调用点零改动
|
||
- `initTables()` 的 CREATE TABLE IF NOT EXISTS 与幂等补列原样保留(兼容旧库)
|
||
- `cli.js`:同套改写(P1-11 统一后只需改 db.js 一处)
|
||
- `routes/import.js`:上传文件先落临时路径再 `new Database(tmpPath)` 只读打开,`exec` 数组结果改对象结果,用后 close
|
||
- **数据迁移**:零迁移——现有 `data/rainweb.db` 是标准 SQLite 文件,直接打开
|
||
- 风险:中;验证:备份 data/ 后启动 → 管理后台 CRUD 走一遍 → `node cli.js status/password` → `sqlite3 data/rainweb.db "PRAGMA integrity_check;"`
|
||
|
||
### P2-16 死代码 + 前端重复清理
|
||
- 删除 `routes/links.js`(引用不存在的 links 表,挂载即崩)、`public/js/main.js`(无引用)、`public/js/passwords.js` 内重复的 `deleteFromDetail`
|
||
- S3-1:`register.html:85` 与 `:92` 重复调用 `CAPTCHA.checkRequired('register')`,删一行
|
||
- S3-2:`escapeHtml/closeDialog/openDialog/showSnackbar` 在 nav/render/blog/forum/admin/passwords/index/profile 各定义一份 → 收敛到 nav.js(或公共 util)单份定义;admin.js 同文件内 6 个函数重复定义(setNavStyle/setCardStyle/previewWallpaperUrl/removeWallpaper/loadWallpaperList/selectUploadedWallpaper/uploadWallpaper,admin.js:179/263 等)删后一份
|
||
- 风险:低;验证:全站走查控制台无 404、无重复定义报错
|
||
|
||
### P2-17 版本单源 + 文档同步
|
||
- `VERSION` 文件为唯一源(server.js:81 已读它);`cli.js` HELP 版本改读 VERSION;`package.json` version 与 VERSION 同步
|
||
- `README.md`:Node 要求改 `>= 22 LTS`;删除更新/部署章节中 GitHub 引用
|
||
- `AGENTS.md`:数据层章节重写(better-sqlite3、无 saveDb、单文件直写、cli.js 已统一)
|
||
- 风险:低;验证:`node cli.js help` 与 `/api/version` 显示一致
|
||
|
||
### P2-18 可选加固(H 系列,按需确认)
|
||
- H1:`helmet()` + 收紧 CSP(CDN 引入 marked 的 5 个页面加 SRI)——建议做
|
||
- H3:验证码 `Math.random()` → `crypto.randomInt`(auth.js:80、email.js:63、profile.js:68)——建议做,改动极小
|
||
- H9:bcrypt cost 10 → 12;注册用户名格式校验——建议做
|
||
- H2:JWT 从 localStorage 改 HttpOnly cookie——改动大(前端 api.js 全量),**默认不做**,列入后续
|
||
- H4:首启随机管理员密码——与 setup 向导冲突,**默认不做**(P0-3 已封堵接管面)
|
||
|
||
### P2-19 前端路由协议收编(方案 B,约 1.5-2 天,依赖 P1-14)
|
||
> 方案 A 修完当前全部可复现 bug;方案 B 是防复发投资——解决"下一次加页面还会不会踩坑"。
|
||
- **B1 单一注册表**:`PAGES` 配置对象 `path → { pjax, title, scripts, global, init }`,`PJAX_PATHS`/`_pageConfig`/脚本依赖全部由它派生;命中且 `pjax:true` 才拦截,其余整页导航(无兜底分支)
|
||
- **B2 页面结构规范**:每个 PJAX 页 `<main data-page="blog">`,router 读 `data-page` 查注册表,不再硬编码路径数组;共享依赖在注册表声明,`_loadScript` 按"已加载集合"增量补齐
|
||
- **B3 页面逻辑全外部化**:HOMEPAGE(P1-14 A3 已抽)、profile 逻辑(profile.html:43-180)抽为独立 js;内联 `<script>` 仅保留极少量数据初始化;统一 `window[global].init()` 协议
|
||
- **B4 pushState/popstate 统一由 router 管理**:页面不再自行 pushState 非标准 state;论坛/博客详情作为"子路由"由注册表定义,popstate 由 router 解析还原(根治 S2-2/S2-3)
|
||
- **B5 生命周期钩子**:注册表条目可选 `destroy()`,音乐嵌入/定时器/事件解绑在此处理(根治 S2-1 结构性累积)
|
||
- 整页应用(login/register/write/embed/forum-manage/setup)标记 `pjax:false` 不参与 PJAX,保留内联脚本
|
||
- 风险:中(popstate 语义变化与音乐嵌入销毁/重建是重点回归面;11 个 html 全动,必须全导航路径走查)
|
||
- 验证:全导航路径走查(每页进出、后退/前进、刷新直链、登录/退出、音乐嵌入显示与自动隐藏);控制台零报错
|
||
- 明确不做(方案 C):引入构建链/框架——违反无构建约定,对单人无测试项目负收益
|
||
|
||
---
|
||
|
||
```
|
||
P0-1..P0-5 (独立,并行) ≈ 半天
|
||
P1-6..P1-10 (安全加固,独立并行)
|
||
P1-11 cli统一 → P1-12 schema_version ≈ 1-2 天
|
||
P1-13 / P1-14(路由止血,独立) ≈ 0.5 天
|
||
P2-15 better-sqlite3(依赖 P1-11/12)≈ 1 天
|
||
P2-16 / P2-17 / P2-18(独立)
|
||
P2-19 路由协议收编(依赖 P1-14) ≈ 1.5-2 天
|
||
```
|
||
|
||
> 若时间有限:P0 + P1 全部完成后项目即处于"安全 + 可正常使用"状态(路由 8 个 bug 全修);P2-19 是防复发投资,可最后做或延后。
|
||
|
||
## 交付节奏
|
||
|
||
每个 P 阶段结束:跑通验证清单 → git commit → 可随时暂停(数据文件备份在手,sql.js→better-sqlite3 前任何时刻可回滚)。
|