mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
fix(guest): normalize guestMode config on load + strict-boolean authorize gate
loadConfig now sanitizes guestMode the same way the write path does: bots is coerced to "all" | string[] (numbers/objects/missing fall back to the default "all"), and permissions are rebuilt from defaults with each known flag strict-coerced to a boolean so a hand-edited/legacy/corrupt config.json can no longer crash the gate or leak garbage index keys. The authorize guest gate now uses === true instead of a truthy check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
952f1fbad3
commit
66230e6b43
4 files changed
+84
-9
No files matched your search
@@ -130,4 +130,50 @@ describe("guestMode config", () => {
|
||||
expect(c.guestMode.permissions.skip).toBe(false); // back-filled default
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// --- B1: loadConfig must sanitize a hand-edited/legacy/corrupt guestMode ---
|
||||
|
||||
function loadGuestMode(raw: unknown) {
|
||||
const dir = mkdtempSync(join(tmpdir(), "tsmb-cfg-"));
|
||||
const p = join(dir, "config.json");
|
||||
writeFileSync(p, JSON.stringify(raw));
|
||||
try {
|
||||
return loadConfig(p).guestMode;
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe("bots normalization", () => {
|
||||
it("a numeric bots value falls back to the default \"all\" (no crash)", () => {
|
||||
const gm = loadGuestMode({ guestMode: { bots: 5 } });
|
||||
expect(gm.bots).toBe("all");
|
||||
});
|
||||
it("an array bots value is filtered to strings only", () => {
|
||||
const gm = loadGuestMode({ guestMode: { bots: ["a", 2, "b"] } });
|
||||
expect(gm.bots).toEqual(["a", "b"]);
|
||||
});
|
||||
it("the literal \"all\" is preserved", () => {
|
||||
const gm = loadGuestMode({ guestMode: { bots: "all" } });
|
||||
expect(gm.bots).toBe("all");
|
||||
});
|
||||
});
|
||||
|
||||
describe("permissions coercion", () => {
|
||||
it("a non-boolean truthy flag is coerced to false; a real true stays true", () => {
|
||||
const gm = loadGuestMode({ guestMode: { permissions: { skip: 1, playNext: true } } });
|
||||
expect(gm.permissions.skip).toBe(false);
|
||||
expect(gm.permissions.playNext).toBe(true);
|
||||
});
|
||||
it("a string permissions value yields defaults with no numeric index keys", () => {
|
||||
const gm = loadGuestMode({ guestMode: { permissions: "hacked" } });
|
||||
// 7 known flags present at their defaults
|
||||
expect(gm.permissions).toEqual({
|
||||
addToQueue: true, playNext: false, playNow: false,
|
||||
skip: false, transport: false, removeClear: false, playMode: false,
|
||||
});
|
||||
// no garbage index keys leaked from spreading a string
|
||||
expect((gm.permissions as unknown as Record<string, unknown>)["0"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
+33
-8
@@ -1,6 +1,7 @@
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, rmSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import type { BotAccess, GuestPermissions } from "./permissions.js";
|
||||
import { GUEST_PERMISSION_FLAGS } from "./permissions.js";
|
||||
|
||||
export interface GuestModeConfig {
|
||||
enabled: boolean;
|
||||
@@ -72,17 +73,41 @@ export function loadConfig(path: string): BotConfig {
|
||||
try {
|
||||
const raw = readFileSync(path, "utf-8");
|
||||
const partial = JSON.parse(raw) as Partial<BotConfig>;
|
||||
|
||||
// Normalize/sanitize guestMode on load. The WRITE path (POST /api/bot/settings)
|
||||
// sanitizes too, but a hand-edited/legacy/corrupt config.json reaches the gate
|
||||
// directly — so coerce it here as well, mirroring that write-path logic.
|
||||
const partialGm = (partial.guestMode ?? {}) as Partial<GuestModeConfig>;
|
||||
const gm: GuestModeConfig = {
|
||||
...defaults.guestMode,
|
||||
...partialGm,
|
||||
// bots → "all" | string[]; anything else falls back to the default ("all").
|
||||
bots:
|
||||
partialGm.bots === "all"
|
||||
? "all"
|
||||
: Array.isArray(partialGm.bots)
|
||||
? partialGm.bots.filter((id): id is string => typeof id === "string")
|
||||
: defaults.guestMode.bots,
|
||||
// permissions → defaults, then spread ONLY a plain object, then strict-coerce
|
||||
// each known flag to a boolean (drops index keys + non-boolean values).
|
||||
permissions: { ...defaults.guestMode.permissions },
|
||||
};
|
||||
const partialPerms = partialGm.permissions;
|
||||
if (
|
||||
partialPerms !== null &&
|
||||
typeof partialPerms === "object" &&
|
||||
!Array.isArray(partialPerms)
|
||||
) {
|
||||
Object.assign(gm.permissions, partialPerms);
|
||||
}
|
||||
for (const f of GUEST_PERMISSION_FLAGS) {
|
||||
gm.permissions[f] = gm.permissions[f] === true;
|
||||
}
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...partial,
|
||||
guestMode: {
|
||||
...defaults.guestMode,
|
||||
...(partial.guestMode ?? {}),
|
||||
permissions: {
|
||||
...defaults.guestMode.permissions,
|
||||
...(partial.guestMode?.permissions ?? {}),
|
||||
},
|
||||
},
|
||||
guestMode: gm,
|
||||
};
|
||||
} catch {
|
||||
return defaults;
|
||||
|
||||
@@ -30,4 +30,8 @@ describe("authorize", () => {
|
||||
it("guest is denied on routes with no guestFlag (e.g. play-song)", () => {
|
||||
expect(run({ role: "guest", guest: { addToQueue: true } }, { capability: "player.control" }).res.statusCode).toBe(403);
|
||||
});
|
||||
it("guest with a non-boolean truthy flag value (1) is denied (strict-boolean gate)", () => {
|
||||
expect(run({ role: "guest", guest: { playNext: 1 } as any }, { guestFlag: "playNext" }).res.statusCode).toBe(403);
|
||||
expect(run({ role: "guest", guest: { playNext: true } }, { guestFlag: "playNext" }).next).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -19,7 +19,7 @@ export function authorize<P = Record<string, string>>(opts: {
|
||||
if (!user) { res.status(401).json({ error: "unauthenticated" }); return; }
|
||||
if (user.role === "admin") { next(); return; }
|
||||
if (user.role === "guest") {
|
||||
if (opts.guestFlag && user.guest?.[opts.guestFlag]) { next(); return; }
|
||||
if (opts.guestFlag && user.guest?.[opts.guestFlag] === true) { next(); return; }
|
||||
res.status(403).json({ error: "forbidden" });
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user