From 740d2adaf3a847ec147b1c3351aac4315ffeb0be Mon Sep 17 00:00:00 2001 From: LukeGus Date: Wed, 7 Oct 2026 21:18:08 -0500 Subject: [PATCH] fix: keep global settings out of non-admin data exports (GHSA-v3vp-ffww-q7pw) --- src/backend/database/database.ts | 23 +++++++--------- .../tests/utils/export-settings.test.ts | 23 ++++++++++++++++ src/backend/utils/export-settings.ts | 26 +++++++++++++++++++ 3 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 src/backend/tests/utils/export-settings.test.ts create mode 100644 src/backend/utils/export-settings.ts diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 0140c70c8..c684918ff 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -81,6 +81,10 @@ import { } from "./repositories/factory.js"; import { withCurrentSqliteForeignKeysDisabled } from "./repositories/sqlite-foreign-keys.js"; import { applyPluginHostImportSettings } from "./routes/host-plugin-settings.js"; +import { + selectExportableSettings, + type SettingData, +} from "../utils/export-settings.js"; import { keepUsableProtocolCredentials, listProtocolLogins, @@ -107,19 +111,9 @@ const requireAdmin = authManager.createAdminMiddleware(); app.use(createCompressionMiddleware()); app.use(createCorsMiddleware()); -type SettingData = { - key: string; - value: string; -}; - -function shouldExportSetting(key: string): boolean { - return !key.startsWith("reset_code_") && !key.startsWith("temp_reset_token_"); -} - -async function getExportableSettings(): Promise { +async function getExportableSettings(isAdmin: boolean): Promise { const settingsRows = await createCurrentSettingsRepository().listAll(); - - return settingsRows.filter((setting) => shouldExportSetting(setting.key)); + return selectExportableSettings(settingsRows, isAdmin); } function writeSettingsToExportDatabase( @@ -1047,7 +1041,10 @@ app.post("/database/export", authenticateJWT, async (req, res) => { ); } - writeSettingsToExportDatabase(exportDb, await getExportableSettings()); + writeSettingsToExportDatabase( + exportDb, + await getExportableSettings(!!user.isAdmin), + ); await writeHostDefaultsToExport(exportDb, userId); } finally { exportDb.close(); diff --git a/src/backend/tests/utils/export-settings.test.ts b/src/backend/tests/utils/export-settings.test.ts new file mode 100644 index 000000000..7a5d04fb1 --- /dev/null +++ b/src/backend/tests/utils/export-settings.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { selectExportableSettings } from "../../utils/export-settings.js"; + +const rows = [ + { key: "allow_registration", value: "true" }, + { key: "audit_log_forward_token", value: "secret" }, + { key: "reset_code_alice", value: "123456" }, + { key: "temp_reset_token_alice", value: "abc" }, + { key: "user_dek_v3_u1", value: "{}" }, +]; + +describe("selectExportableSettings", () => { + it("gives a non-admin export no global settings", () => { + expect(selectExportableSettings(rows, false)).toEqual([]); + }); + + it("keeps instance config for admins but drops secrets", () => { + expect(selectExportableSettings(rows, true).map((r) => r.key)).toEqual([ + "allow_registration", + "user_dek_v3_u1", + ]); + }); +}); diff --git a/src/backend/utils/export-settings.ts b/src/backend/utils/export-settings.ts new file mode 100644 index 000000000..154a0ac65 --- /dev/null +++ b/src/backend/utils/export-settings.ts @@ -0,0 +1,26 @@ +export type SettingData = { + key: string; + value: string; +}; + +const SECRET_SETTING_PREFIXES = [ + "reset_code_", + "temp_reset_token_", + "audit_log_forward_token", +]; + +/** + * Global settings are instance config, not user data: only an admin's export + * carries them (import only applies them for admins anyway), and never the + * one-time reset codes or the audit forwarding token. + */ +export function selectExportableSettings( + rows: SettingData[], + isAdmin: boolean, +): SettingData[] { + if (!isAdmin) return []; + return rows.filter( + (row) => + !SECRET_SETTING_PREFIXES.some((prefix) => row.key.startsWith(prefix)), + ); +}