From 49a4ad31e422fc306506b9c194f701c4e85ea02f Mon Sep 17 00:00:00 2001 From: Xiangzhe Date: Fri, 21 Aug 2026 13:05:05 -0300 Subject: [PATCH] fix(security): require auth for db-backups export/import under requireLogin=false /api/db-backups/export and /import sat outside ALWAYS_PROTECTED_API_PATHS, so with requireLogin=false an anonymous caller could stream the full SQLite database (api_keys, provider credentials, OAuth tokens) or replace it wholesale. Adding /api/db-backups to the Tier-2 allowlist requires a credential for all three sibling routes, matching the trade-off /api/settings/database already makes. Reported by @ntdat812 via GHSA-mghq-58h3-qcqj. --- src/server/authz/routeGuard.ts | 5 +++++ tests/unit/authz/routeGuard.test.ts | 9 +++++++++ 2 files changed, 14 insertions(+) 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);