# 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/.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 页) - 验证:发帖内容含 `` → 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 只替换 `
`、目标页 `