From 68416366ebf1f71cce5a2b581590638f3aa89481 Mon Sep 17 00:00:00 2001 From: zqlit Date: Mon, 5 Oct 2026 14:41:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(editor-api):=20=E8=BA=AB=E4=BB=BD=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E6=94=B9=E4=B8=BA=E4=BC=9A=E8=AF=9D=E4=BC=98=E5=85=88?= =?UTF-8?q?=EF=BC=8C=E9=98=B2=E6=AD=A2=E9=85=8D=E7=BD=AE=E6=BC=82=E7=A7=BB?= =?UTF-8?q?=E9=9D=99=E9=BB=98=E6=8F=90=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit identify() 原来「令牌优先」:令牌对得上就用 X-Editor-* 头的身份,没有头时默认 admin。 国内线路的 nginx 目前不注入令牌,但一旦配置回滚到「注入令牌」的旧版本,编辑的请求 会命中令牌分支 → 静默获得管理员权限(看到全部文章、能改别人的、能全量发布)。 改为「会话(真实的人)优先于共享令牌(服务身份)」,把边界写死在代码里而不是靠配置。 新增断言:编辑会话 + 管理员令牌头 → 仍按编辑身份。role-perm.mjs 47/47 --- editor-api/server.mjs | 22 +++++++++++++++------- editor-api/test/role-perm.mjs | 7 +++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/editor-api/server.mjs b/editor-api/server.mjs index 91c77156..0d72b288 100644 --- a/editor-api/server.mjs +++ b/editor-api/server.mjs @@ -218,6 +218,20 @@ function decodeHeader(v) { * ② 浏览器直连本机后台:身份就在自己的 HttpOnly Cookie 会话里。 */ function identify(req) { + // ★ 顺序很重要:会话(具体某个人的登录态)优先于共享令牌(服务身份)。 + // 共享令牌分支的默认角色是 admin —— 国内线路的 nginx 目前不注入令牌, + // 但一旦配置漂移回「nginx 注入令牌」的旧版本,令牌优先会让编辑直接拿到 + // 管理员权限(看到全部文章、能改别人的、能全量发布),而且是静默的。 + // 靠配置保证安全太脆弱,这里用「人优先于机器」把边界写死在代码里。 + const s = readSession(req); + if (s) { + return { + uid: String(s.uid || ''), + name: String(s.name || cfg.adminUser), + role: s.role === 'editor' ? 'editor' : 'admin', + }; + } + // Worker 反代通道:由 Cloudflare 发出,不带浏览器 Cookie,只有令牌 + 身份头 if (safeEqual(req.headers['x-editor-token'] || '', cfg.token)) { return { uid: String(req.headers['x-editor-uid'] || '').trim(), @@ -225,13 +239,7 @@ function identify(req) { role: String(req.headers['x-editor-role'] || '').trim() === 'editor' ? 'editor' : 'admin', }; } - const s = readSession(req); - if (!s) return null; - return { - uid: String(s.uid || ''), - name: String(s.name || cfg.adminUser), - role: s.role === 'editor' ? 'editor' : 'admin', - }; + return null; } /** diff --git a/editor-api/test/role-perm.mjs b/editor-api/test/role-perm.mjs index f908dd0d..be1bcd33 100644 --- a/editor-api/test/role-perm.mjs +++ b/editor-api/test/role-perm.mjs @@ -413,6 +413,13 @@ async function main() { // 到这一步编辑名下有 3 篇了:自己的、按昵称认领的老文章、以及第 ④ 节新建的那篇 check('用这枚 Cookie 列文章 → 只看到自己的 3 篇', r.status === 200 && r.data.total === 3, 'total=' + r.data.total); + // ★ 会话优先于共享令牌。国内线路的 nginx 目前不注入令牌,但万一配置漂移回 + // 「注入令牌」的旧版本,请求会同时带着管理员身份的令牌头 + 编辑的浏览器会话。 + // 此时必须以「人」为准 —— 否则编辑会静默拿到管理员权限。 + r = await call('GET', '/posts', { cookie: edCookie, ident: ADMIN }); + check('编辑会话 + 管理员令牌头 → 仍按编辑身份(只看自己 3 篇)', + r.status === 200 && r.data.total === 3, 'total=' + r.data.total); + // 把签名载荷里的 r 改成 admin(签名仍是按 editor 算的)→ 必须拒 r = await call('GET', `/admin/handoff?ts=${String(Date.now())}&u=7&n=%E5%B0%8F%E7%BE%8E&r=admin&t=${sigFor('7', '小美', 'editor')}`); check('把角色改成 admin 但签名不匹配 → 403', r.status === 403, 'got ' + r.status);