diff --git a/src/server/authz/routeGuard.ts b/src/server/authz/routeGuard.ts index 4bb072d5cb..d59e68836f 100644 --- a/src/server/authz/routeGuard.ts +++ b/src/server/authz/routeGuard.ts @@ -119,6 +119,11 @@ export const ALWAYS_PROTECTED_API_PATHS: ReadonlyArray = [ "/api/shutdown", "/api/providers/health-autopilot/actions", "/api/settings/database", + // Full-database export/import: a credential dump and an irreversible replace. + // Must stay authenticated even under requireLogin=false, for the same reason + // /api/settings/database already does. isAlwaysProtectedPath matches on a path + // boundary, so this covers export, exportAll and import. (GHSA-mghq-58h3-qcqj) + "/api/db-backups", ]; export function isLoopbackHost(hostHeader: string | null): boolean { diff --git a/tests/unit/authz/routeGuard.test.ts b/tests/unit/authz/routeGuard.test.ts index 2a4fd3799c..9d2e24672b 100644 --- a/tests/unit/authz/routeGuard.test.ts +++ b/tests/unit/authz/routeGuard.test.ts @@ -58,6 +58,15 @@ test("isAlwaysProtectedPath: /api/settings/database is always protected", () => assert.equal(isAlwaysProtectedPath("/api/settings/database"), true); }); +test("isAlwaysProtectedPath: /api/db-backups is always protected (GHSA-mghq-58h3-qcqj)", () => { + // Full-database read/replace must require auth even when requireLogin=false — + // the same Tier-2 trade-off /api/settings/database already makes. The single + // prefix entry covers export, exportAll, import and future siblings. + assert.equal(isAlwaysProtectedPath("/api/db-backups/export"), true); + assert.equal(isAlwaysProtectedPath("/api/db-backups/exportAll"), true); + assert.equal(isAlwaysProtectedPath("/api/db-backups/import"), true); +}); + test("isAlwaysProtectedPath: ordinary settings routes are not always protected", () => { assert.equal(isAlwaysProtectedPath("/api/settings"), false); assert.equal(isAlwaysProtectedPath("/api/settings/proxy"), false);