fix(backup): redact destination secrets in settings

This commit is contained in:
shuaiplus
2026-07-06 13:50:49 +08:00
parent 2df43ccdb0
commit 00e0ec0892
2 changed files with 63 additions and 5 deletions
+6 -4
View File
@@ -19,6 +19,7 @@ import {
loadBackupSettings, loadBackupSettings,
normalizeBackupSettingsInput, normalizeBackupSettingsInput,
normalizeImportedBackupSettings, normalizeImportedBackupSettings,
redactBackupSettingsSecrets,
repairBackupSettings, repairBackupSettings,
requireBackupDestination, requireBackupDestination,
saveBackupSettings, saveBackupSettings,
@@ -675,6 +676,7 @@ function collectExternalRemoteAttachmentBlobNames(archiveBytes: Uint8Array): str
function toImportStatusCode(message: string): number { function toImportStatusCode(message: string): number {
const lower = message.toLowerCase(); const lower = message.toLowerCase();
if (lower.includes('checksum')) return 400; if (lower.includes('checksum')) return 400;
if (lower.includes('invalid remote backup path') || lower.includes('please select a backup zip file')) return 409;
if (lower.includes('invalid backup') || lower.includes('invalid json')) return 400; if (lower.includes('invalid backup') || lower.includes('invalid json')) return 400;
if (lower.includes('fresh instance')) return 409; if (lower.includes('fresh instance')) return 409;
if (lower.includes('not configured') || lower.includes('kv')) return 409; if (lower.includes('not configured') || lower.includes('kv')) return 409;
@@ -858,7 +860,7 @@ export async function handleGetAdminBackupSettings(request: Request, env: Env, a
const storage = new StorageService(env.DB); const storage = new StorageService(env.DB);
try { try {
const settings = await loadBackupSettings(storage, env, 'UTC'); const settings = await loadBackupSettings(storage, env, 'UTC');
return jsonResponse(settings); return jsonResponse(redactBackupSettingsSecrets(settings));
} catch (error) { } catch (error) {
return errorResponse(error instanceof Error ? error.message : 'Backup settings could not be loaded', 409); return errorResponse(error instanceof Error ? error.message : 'Backup settings could not be loaded', 409);
} }
@@ -897,7 +899,7 @@ export async function handleUpdateAdminBackupSettings(request: Request, env: Env
destinationCount: next.destinations.length, destinationCount: next.destinations.length,
scheduledDestinationCount: next.destinations.filter((destination) => destination.schedule.enabled).length, scheduledDestinationCount: next.destinations.filter((destination) => destination.schedule.enabled).length,
}, request); }, request);
return jsonResponse(next); return jsonResponse(redactBackupSettingsSecrets(next));
} }
export async function handleGetAdminBackupSettingsRepairState(request: Request, env: Env, actorUser: User): Promise<Response> { export async function handleGetAdminBackupSettingsRepairState(request: Request, env: Env, actorUser: User): Promise<Response> {
@@ -950,7 +952,7 @@ export async function handleRepairAdminBackupSettings(request: Request, env: Env
destinationCount: next.destinations.length, destinationCount: next.destinations.length,
scheduledDestinationCount: next.destinations.filter((destination) => destination.schedule.enabled).length, scheduledDestinationCount: next.destinations.filter((destination) => destination.schedule.enabled).length,
}, request); }, request);
return jsonResponse(next); return jsonResponse(redactBackupSettingsSecrets(next));
} }
export async function handleRunAdminConfiguredBackup(request: Request, env: Env, actorUser: User): Promise<Response> { export async function handleRunAdminConfiguredBackup(request: Request, env: Env, actorUser: User): Promise<Response> {
@@ -987,7 +989,7 @@ export async function handleRunAdminConfiguredBackup(request: Request, env: Env,
provider: outcome.result.provider, provider: outcome.result.provider,
remotePath: outcome.result.remotePath, remotePath: outcome.result.remotePath,
}, },
settings: outcome.settings, settings: redactBackupSettingsSecrets(outcome.settings),
}); });
} catch (error) { } catch (error) {
return errorResponse(error instanceof Error ? error.message : 'Backup run failed', 500); return errorResponse(error instanceof Error ? error.message : 'Backup run failed', 500);
+57 -1
View File
@@ -28,6 +28,7 @@ import {
export const BACKUP_SETTINGS_CONFIG_KEY = 'backup.settings.v1'; export const BACKUP_SETTINGS_CONFIG_KEY = 'backup.settings.v1';
const BACKUP_RUNTIME_CONFIG_KEY = 'backup.runtime.v1'; const BACKUP_RUNTIME_CONFIG_KEY = 'backup.runtime.v1';
export const BACKUP_SCHEDULER_WINDOW_MINUTES = 5; export const BACKUP_SCHEDULER_WINDOW_MINUTES = 5;
export const REDACTED_BACKUP_SECRET = '********';
const MAX_BACKUP_DESTINATIONS = 24; const MAX_BACKUP_DESTINATIONS = 24;
export type { export type {
@@ -180,6 +181,32 @@ function normalizeDestination(
return normalizeWebDavDestination(destination, allowIncomplete); return normalizeWebDavDestination(destination, allowIncomplete);
} }
function shouldPreserveBackupSecret(value: unknown): boolean {
if (value === undefined || value === null) return true;
const raw = String(value);
return raw === '' || raw === REDACTED_BACKUP_SECRET;
}
function withPreservedDestinationSecret(
destinationType: BackupDestinationType,
inputDestination: unknown,
previous: BackupDestinationRecord | undefined
): unknown {
const source = isPlainObject(inputDestination) ? { ...inputDestination } : {};
if (destinationType === 's3') {
const previousDestination = previous?.type === 's3' ? previous.destination as S3BackupDestination : null;
if (shouldPreserveBackupSecret(source.secretAccessKey)) {
source.secretAccessKey = previousDestination?.secretAccessKey || '';
}
} else {
const previousDestination = previous?.type === 'webdav' ? previous.destination as WebDavBackupDestination : null;
if (shouldPreserveBackupSecret(source.password)) {
source.password = previousDestination?.password || '';
}
}
return source;
}
function normalizeRuntime(value: unknown): BackupRuntimeState { function normalizeRuntime(value: unknown): BackupRuntimeState {
const source = isPlainObject(value) ? value : {}; const source = isPlainObject(value) ? value : {};
const asIso = (input: unknown): string | null => { const asIso = (input: unknown): string | null => {
@@ -250,7 +277,11 @@ function normalizeDestinationRecord(
retentionCount: normalizeRetentionCount(retentionSource, previousSchedule.retentionCount), retentionCount: normalizeRetentionCount(retentionSource, previousSchedule.retentionCount),
}; };
const destination = normalizeDestination(type, input.destination, !schedule.enabled); const destination = normalizeDestination(
type,
withPreservedDestinationSecret(type, input.destination, previous),
!schedule.enabled
);
return { return {
id, id,
@@ -432,6 +463,31 @@ export function serializeBackupSettings(settings: BackupSettings): string {
return JSON.stringify(stripRuntimeFromSettings(settings)); return JSON.stringify(stripRuntimeFromSettings(settings));
} }
export function redactBackupSettingsSecrets(settings: BackupSettings): BackupSettings {
return {
destinations: settings.destinations.map((destination) => {
if (destination.type === 's3') {
const config = destination.destination as S3BackupDestination;
return {
...destination,
destination: {
...config,
secretAccessKey: config.secretAccessKey ? REDACTED_BACKUP_SECRET : '',
},
};
}
const config = destination.destination as WebDavBackupDestination;
return {
...destination,
destination: {
...config,
password: config.password ? REDACTED_BACKUP_SECRET : '',
},
};
}),
};
}
export async function loadBackupSettings(storage: StorageService, env: Env, fallbackTimezone: string = 'UTC'): Promise<BackupSettings> { export async function loadBackupSettings(storage: StorageService, env: Env, fallbackTimezone: string = 'UTC'): Promise<BackupSettings> {
const raw = await storage.getConfigValue(BACKUP_SETTINGS_CONFIG_KEY); const raw = await storage.getConfigValue(BACKUP_SETTINGS_CONFIG_KEY);
const mergeRuntime = async (settings: BackupSettings): Promise<BackupSettings> => ( const mergeRuntime = async (settings: BackupSettings): Promise<BackupSettings> => (