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,
normalizeBackupSettingsInput,
normalizeImportedBackupSettings,
redactBackupSettingsSecrets,
repairBackupSettings,
requireBackupDestination,
saveBackupSettings,
@@ -675,6 +676,7 @@ function collectExternalRemoteAttachmentBlobNames(archiveBytes: Uint8Array): str
function toImportStatusCode(message: string): number {
const lower = message.toLowerCase();
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('fresh instance')) 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);
try {
const settings = await loadBackupSettings(storage, env, 'UTC');
return jsonResponse(settings);
return jsonResponse(redactBackupSettingsSecrets(settings));
} catch (error) {
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,
scheduledDestinationCount: next.destinations.filter((destination) => destination.schedule.enabled).length,
}, request);
return jsonResponse(next);
return jsonResponse(redactBackupSettingsSecrets(next));
}
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,
scheduledDestinationCount: next.destinations.filter((destination) => destination.schedule.enabled).length,
}, request);
return jsonResponse(next);
return jsonResponse(redactBackupSettingsSecrets(next));
}
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,
remotePath: outcome.result.remotePath,
},
settings: outcome.settings,
settings: redactBackupSettingsSecrets(outcome.settings),
});
} catch (error) {
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';
const BACKUP_RUNTIME_CONFIG_KEY = 'backup.runtime.v1';
export const BACKUP_SCHEDULER_WINDOW_MINUTES = 5;
export const REDACTED_BACKUP_SECRET = '********';
const MAX_BACKUP_DESTINATIONS = 24;
export type {
@@ -180,6 +181,32 @@ function normalizeDestination(
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 {
const source = isPlainObject(value) ? value : {};
const asIso = (input: unknown): string | null => {
@@ -250,7 +277,11 @@ function normalizeDestinationRecord(
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 {
id,
@@ -432,6 +463,31 @@ export function serializeBackupSettings(settings: BackupSettings): string {
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> {
const raw = await storage.getConfigValue(BACKUP_SETTINGS_CONFIG_KEY);
const mergeRuntime = async (settings: BackupSettings): Promise<BackupSettings> => (