fix(editor): 编辑器接口只认真实会话,封掉 name/email 匿名兜底越权(P0)
isAdminRequest 的 query 兜底会把「读文章源码/改文件/删文章/触发 git 发布」 裸露给任何拼参数的人。改为 requireAdminSession:只认 Bearer 会话, 已登录 + is_admin(或邮箱命中配置管理员)才放行。线上 Worker 已部署此版本。
This commit is contained in:
1 parent
a4758dbbfd
commit
a11631faa6
1 file changed
+29
-4
@@ -12,10 +12,35 @@
|
|||||||
*/
|
*/
|
||||||
import type { Ctx } from '../router';
|
import type { Ctx } from '../router';
|
||||||
import { fail, json } from '../lib/util';
|
import { fail, json } from '../lib/util';
|
||||||
import { isAdminRequest } from '../lib/session';
|
import { getAdminUsers } from '../lib/db';
|
||||||
|
import { userFromToken } from '../lib/session';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ★ 鉴权只认「真实登录会话(Bearer token)」,**绝不能**用 isAdminRequest。
|
||||||
|
*
|
||||||
|
* isAdminRequest 里有一条 Artalk 为老客户端留的兜底:请求带
|
||||||
|
* `?name=<管理员名>&email=<管理员邮箱>` 就视为管理员。而 ADMIN_NAME /
|
||||||
|
* ADMIN_EMAIL 是写死在 wrangler.toml、并暴露在后台页面里的公开信息——
|
||||||
|
* 一旦这条兜底被用在编辑器路由上,等于把「读全部文章源码 + 改文件 +
|
||||||
|
* 删文章 + 传图 + 触发 git 发布(真推到 CNB/GitHub)」的能力裸露在公网,
|
||||||
|
* 任何人拼一个 query 就能删你的博客。这里必须硬性要求会话。
|
||||||
|
*
|
||||||
|
* 已登录用户的判定与 isAdminRequest 一致(is_admin 或邮箱命中配置管理员),
|
||||||
|
* 只是砍掉了「匿名 + query 参数」那一条。
|
||||||
|
*/
|
||||||
|
async function requireAdminSession(ctx: Ctx) {
|
||||||
|
const user = ctx.user ?? (await userFromToken(ctx.env, ctx.req.headers.get('Authorization')));
|
||||||
|
if (!user) return null;
|
||||||
|
if (user.is_admin) return user;
|
||||||
|
// 已登录但没打 is_admin 标的老账号:邮箱命中配置里的管理员也算
|
||||||
|
const admins = await getAdminUsers(ctx.env);
|
||||||
|
return admins.some((a) => a.email && a.email.toLowerCase() === String(user.email || '').toLowerCase())
|
||||||
|
? user
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
async function guard(ctx: Ctx): Promise<Response | null> {
|
async function guard(ctx: Ctx): Promise<Response | null> {
|
||||||
if (!(await isAdminRequest(ctx.env, ctx.req, ctx.user))) {
|
if (!(await requireAdminSession(ctx))) {
|
||||||
return fail(403, '需要管理员权限');
|
return fail(403, '需要管理员权限');
|
||||||
}
|
}
|
||||||
if (!ctx.env.EDITOR_API_BASE) {
|
if (!ctx.env.EDITOR_API_BASE) {
|
||||||
@@ -136,9 +161,9 @@ export const publish = (ctx: Ctx) => relayJson(ctx, '/git/publish', 'POST');
|
|||||||
|
|
||||||
export const sync = (ctx: Ctx) => relay(ctx, '/git/sync', { method: 'POST' });
|
export const sync = (ctx: Ctx) => relay(ctx, '/git/sync', { method: 'POST' });
|
||||||
|
|
||||||
/** 健康检查:让后台能显示「编辑后端是否在线」 */
|
/** 健康检查:让后台能显示「编辑后端是否在线」(同样只认真实会话) */
|
||||||
export async function health(ctx: Ctx): Promise<Response> {
|
export async function health(ctx: Ctx): Promise<Response> {
|
||||||
if (!(await isAdminRequest(ctx.env, ctx.req, ctx.user))) return fail(403, '需要管理员权限');
|
if (!(await requireAdminSession(ctx))) return fail(403, '需要管理员权限');
|
||||||
if (!ctx.env.EDITOR_API_BASE) return json({ ok: false, reason: '缺少 EDITOR_API_BASE' });
|
if (!ctx.env.EDITOR_API_BASE) return json({ ok: false, reason: '缺少 EDITOR_API_BASE' });
|
||||||
try {
|
try {
|
||||||
const res = await fetch(target(ctx, '/health'), { method: 'GET' });
|
const res = await fetch(target(ctx, '/health'), { method: 'GET' });
|
||||||
|
|||||||
Reference in new issue
Block a user