fix: conditionally include secrets in user data backup (#5360)

* fix: conditionally include secrets in user data backup

* feat: add full data backup toggle

* 418 -> 403
I'm not a teapot

* Distinguish fails from disabled
This commit is contained in:
Cohee
2026-03-28 01:52:03 +02:00
committed by GitHub
parent b06a62952d
commit c78f978ede
7 changed files with 54 additions and 5 deletions
+2
View File
@@ -165,6 +165,8 @@ rateLimiting:
## BACKUP CONFIGURATION
backups:
# Allow users to create a full backup archive of their data
allowFullDataBackup: true
# Common settings for all backup types
common:
# Number of backups to keep for each chat and settings file
Vendored
+1 -1
View File
@@ -40,7 +40,7 @@ declare global {
/**
* Authenticated user handle.
*/
handle: string;
handle: string | null;
/**
* Last time the session was extended.
*/
+23
View File
@@ -277,6 +277,29 @@ function getActiveSecretLabel(key) {
return '';
}
/**
* Checks if secrets can be viewed based on server configuration.
* @returns {Promise<boolean|null>} A boolean value, or null if the request fails.
*/
export async function canViewSecrets() {
try {
const response = await fetch('/api/secrets/settings', {
method: 'POST',
headers: getRequestHeaders({ omitContentType: true }),
});
if (!response.ok) {
return null;
}
const data = await response.json();
return data?.allowKeysExposure === true;
} catch (error) {
console.error('Error getting secrets settings:', error);
return null;
}
}
async function viewSecrets() {
const response = await fetch('/api/secrets/view', {
method: 'POST',
+6
View File
@@ -1,5 +1,6 @@
import { getRequestHeaders } from '../script.js';
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
import { canViewSecrets } from './secrets.js';
import { renderTemplateAsync } from './templates.js';
import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './utils.js';
@@ -266,6 +267,11 @@ async function backupUserData(handle, callback) {
throw new Error('Failed to backup user data');
}
const includesSecrets = await canViewSecrets();
if (includesSecrets === false) {
toastr.warning('The backup will not include secrets due to a server configuration.', 'Secrets Not Included');
}
const blob = await response.blob();
const header = response.headers.get('Content-Disposition');
const parts = header.split(';');
+5 -1
View File
@@ -104,7 +104,7 @@ const EXPORTABLE_KEYS = [
SECRET_KEYS.DEEPLX_URL,
];
const allowKeysExposure = !!getConfigValue('allowKeysExposure', false, 'boolean');
export const allowKeysExposure = !!getConfigValue('allowKeysExposure', false, 'boolean');
/**
* SecretManager class to handle all secret operations
@@ -636,3 +636,7 @@ router.post('/rename', (request, response) => {
return response.sendStatus(500);
}
});
router.post('/settings', async (_request, response) => {
return response.send({ allowKeysExposure });
});
+8 -1
View File
@@ -8,7 +8,7 @@ import express from 'express';
import { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } from '../users.js';
import { SETTINGS_FILE } from '../constants.js';
import { checkForNewContent, CONTENT_TYPES } from './content-manager.js';
import { color, Cache } from '../util.js';
import { color, Cache, getConfigValue } from '../util.js';
const RESET_CACHE = new Cache(5 * 60 * 1000);
@@ -138,6 +138,13 @@ router.post('/change-password', async (request, response) => {
router.post('/backup', async (request, response) => {
try {
const allowFullDataBackup = !!getConfigValue('backups.allowFullDataBackup', true, 'boolean');
if (!allowFullDataBackup) {
console.warn('Backup failed: Full data backup is disabled in configuration');
return response.status(403).json({ error: 'Full data backup is disabled' });
}
const handle = request.body.handle;
if (!handle) {
+9 -2
View File
@@ -17,7 +17,7 @@ import sanitize from 'sanitize-filename';
import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js';
import { getConfigValue, color, delay, generateTimestamp, invalidateFirefoxCache, isPathUnderParent } from './util.js';
import { readSecret, writeSecret } from './endpoints/secrets.js';
import { allowKeysExposure, readSecret, writeSecret, SECRETS_FILE } from './endpoints/secrets.js';
import { getContentOfType } from './endpoints/content-manager.js';
import { serverDirectory } from './server-directory.js';
@@ -1052,7 +1052,14 @@ export async function createBackupArchive(handle, response) {
archive.pipe(response);
// Append files from a sub-directory, putting its contents at the root of archive
archive.directory(directories.root, false);
const ignore = allowKeysExposure ? [] : [SECRETS_FILE];
archive.glob('**/*', {
cwd: directories.root,
follow: false,
stat: true,
dot: true,
ignore,
});
archive.finalize();
}