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

c78f978ede3b94351f70ed8c6df40a89073b53a0

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
7 files changed, +54 -5Ignore whitespace
default/config.yaml+2 -0
@@ -165,6 +165,8 @@ rateLimiting:
165165
166166## BACKUP CONFIGURATION
167167backups:
168+ # Allow users to create a full backup archive of their data
169+ allowFullDataBackup: true
168170 # Common settings for all backup types
169171 common:
170172 # Number of backups to keep for each chat and settings file
index.d.ts+1 -1
@@ -40,7 +40,7 @@ declare global {
4040 /**
4141 * Authenticated user handle.
4242 */
4343 handle: string | null;
4444 /**
4545 * Last time the session was extended.
4646 */
public/scripts/secrets.js+23 -0
@@ -277,6 +277,29 @@ function getActiveSecretLabel(key) {
277277 return '';
278278}
279279
280+/**
281+ * Checks if secrets can be viewed based on server configuration.
282+ * @returns {Promise<boolean|null>} A boolean value, or null if the request fails.
283+ */
284+export async function canViewSecrets() {
285+ try {
286+ const response = await fetch('/api/secrets/settings', {
287+ method: 'POST',
288+ headers: getRequestHeaders({ omitContentType: true }),
289+ });
290+
291+ if (!response.ok) {
292+ return null;
293+ }
294+
295+ const data = await response.json();
296+ return data?.allowKeysExposure === true;
297+ } catch (error) {
298+ console.error('Error getting secrets settings:', error);
299+ return null;
300+ }
301+}
302+
280303async function viewSecrets() {
281304 const response = await fetch('/api/secrets/view', {
282305 method: 'POST',
public/scripts/user.js+6 -0
@@ -1,5 +1,6 @@
11import { getRequestHeaders } from '../script.js';
22import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
3+import { canViewSecrets } from './secrets.js';
34import { renderTemplateAsync } from './templates.js';
45import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './utils.js';
56
@@ -266,6 +267,11 @@ async function backupUserData(handle, callback) {
266267 throw new Error('Failed to backup user data');
267268 }
268269
270+ const includesSecrets = await canViewSecrets();
271+ if (includesSecrets === false) {
272+ toastr.warning('The backup will not include secrets due to a server configuration.', 'Secrets Not Included');
273+ }
274+
269275 const blob = await response.blob();
270276 const header = response.headers.get('Content-Disposition');
271277 const parts = header.split(';');
src/endpoints/secrets.js+5 -1
@@ -104,7 +104,7 @@ const EXPORTABLE_KEYS = [
104104 SECRET_KEYS.DEEPLX_URL,
105105];
106106
107107export const allowKeysExposure = !!getConfigValue('allowKeysExposure', false, 'boolean');
108108
109109/**
110110 * SecretManager class to handle all secret operations
@@ -636,3 +636,7 @@ router.post('/rename', (request, response) => {
636636 return response.sendStatus(500);
637637 }
638638});
639+
640+router.post('/settings', async (_request, response) => {
641+ return response.send({ allowKeysExposure });
642+});
src/endpoints/users-private.js+8 -1
@@ -8,7 +8,7 @@ import express from 'express';
88import { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } from '../users.js';
99import { SETTINGS_FILE } from '../constants.js';
1010import { checkForNewContent, CONTENT_TYPES } from './content-manager.js';
1111import { color, Cache, getConfigValue } from '../util.js';
1212
1313const RESET_CACHE = new Cache(5 * 60 * 1000);
1414
@@ -138,6 +138,13 @@ router.post('/change-password', async (request, response) => {
138138
139139router.post('/backup', async (request, response) => {
140140 try {
141+ const allowFullDataBackup = !!getConfigValue('backups.allowFullDataBackup', true, 'boolean');
142+
143+ if (!allowFullDataBackup) {
144+ console.warn('Backup failed: Full data backup is disabled in configuration');
145+ return response.status(403).json({ error: 'Full data backup is disabled' });
146+ }
147+
141148 const handle = request.body.handle;
142149
143150 if (!handle) {
src/users.js+9 -2
@@ -17,7 +17,7 @@ import sanitize from 'sanitize-filename';
1717
1818import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js';
1919import { getConfigValue, color, delay, generateTimestamp, invalidateFirefoxCache, isPathUnderParent } from './util.js';
2020import { allowKeysExposure, readSecret, writeSecret, SECRETS_FILE } from './endpoints/secrets.js';
2121import { getContentOfType } from './endpoints/content-manager.js';
2222import { serverDirectory } from './server-directory.js';
2323
@@ -1052,7 +1052,14 @@ export async function createBackupArchive(handle, response) {
10521052 archive.pipe(response);
10531053
10541054 // Append files from a sub-directory, putting its contents at the root of archive
1055- archive.directory(directories.root, false);
1055+ const ignore = allowKeysExposure ? [] : [SECRETS_FILE];
1056+ archive.glob('**/*', {
1057+ cwd: directories.root,
1058+ follow: false,
1059+ stat: true,
1060+ dot: true,
1061+ ignore,
1062+ });
10561063 archive.finalize();
10571064}
10581065