fix(settings): serialize GET /api/settings from an allowlist (#1299)

The settings read returned the entire global_settings map minus a small
denylist of auth keys. Other subsystems persist their config in the same
table, so cloud backup values (endpoint, bucket, access key) were returned
to any authenticated user, including read-only roles, bypassing the
redaction the dedicated cloud-backup endpoint applies.

Project the response from the operational allowlist instead, so only the
keys the settings UI reads are returned and any key written to
global_settings by another subsystem is excluded by default. Remove the
now-unused denylist constant.
This commit is contained in:
Anso
2026-06-04 01:49:03 -04:00
committed by GitHub
parent cfadd76159
commit 4596a90474
2 changed files with 75 additions and 13 deletions
+14 -9
View File
@@ -4,12 +4,12 @@ import { DatabaseService } from '../services/DatabaseService';
import { authMiddleware } from '../middleware/auth';
import { requireAdmin } from '../middleware/tierGates';
// Keys that contain auth credentials; never exposed to the frontend or
// writable via the settings API.
const PRIVATE_SETTINGS_KEYS = new Set(['auth_username', 'auth_password_hash', 'auth_jwt_secret']);
// Strict allowlist of keys writable via the settings API. Prevents
// overwriting auth credentials through a misconfigured key.
// Strict allowlist of keys readable and writable via the generic settings
// API. This is the single source of truth for what the endpoint exposes:
// reads project only these keys, so secrets written to global_settings by
// other subsystems (the cloud_backup_* credentials stored by the cloud-backup
// route, the auth_* login secrets) are never returned here; writes are
// rejected for anything outside the list.
const ALLOWED_SETTING_KEYS = new Set([
'host_cpu_limit',
'host_ram_limit',
@@ -47,9 +47,14 @@ export const settingsRouter = Router();
settingsRouter.get('/', authMiddleware, async (_req: Request, res: Response): Promise<void> => {
try {
const settings = { ...DatabaseService.getInstance().getGlobalSettings() };
for (const key of PRIVATE_SETTINGS_KEYS) {
delete settings[key];
const all = DatabaseService.getInstance().getGlobalSettings();
// Project only allowlisted operational keys. A denylist would leak every
// future sensitive key written to global_settings by default (e.g. the
// cloud_backup_* credentials the cloud-backup route stores here); the
// allowlist fails closed.
const settings: Record<string, string> = {};
for (const [key, value] of Object.entries(all)) {
if (ALLOWED_SETTING_KEYS.has(key)) settings[key] = value;
}
res.json(settings);
} catch (error) {