From 39adefb63281fb4d1dd0c20bd8b706b85f104a89 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 18 Apr 2026 17:03:37 -0500 Subject: [PATCH] Update backup load & upload to remove tempfile on failed backups, validate details filesize & close zip --- server/managers/BackupManager.js | 39 +++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/server/managers/BackupManager.js b/server/managers/BackupManager.js index 2697f94ea..a7b531e62 100644 --- a/server/managers/BackupManager.js +++ b/server/managers/BackupManager.js @@ -126,13 +126,31 @@ class BackupManager { } catch (error) { // Not a valid zip file Logger.error('[BackupManager] Failed to read backup file - backup might not be a valid .zip file', tempPath, error) + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) return res.status(400).send('Failed to read backup file - backup might not be a valid .zip file') } - if (!Object.keys(entries).includes('absdatabase.sqlite')) { + if (!entries['absdatabase.sqlite']) { Logger.error(`[BackupManager] Invalid backup with no absdatabase.sqlite file - might be a backup created on an old Audiobookshelf server.`) + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) return res.status(500).send('Invalid backup with no absdatabase.sqlite file - might be a backup created on an old Audiobookshelf server.') } + const detailsEntry = entries['details'] + if (!detailsEntry) { + Logger.error('[BackupManager] Invalid backup - missing details entry') + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) + return res.status(400).send('Invalid backup file - missing details entry') + } + if (detailsEntry.size > 1024 * 1024) { + Logger.error(`[BackupManager] Backup details entry too large: ${detailsEntry.size} bytes`) + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) + return res.status(400).send('Invalid backup file - details entry too large') + } + const data = await zip.entryData('details') const details = data.toString('utf8').split('\n') @@ -140,9 +158,13 @@ class BackupManager { if (!backup.serverVersion) { Logger.error(`[BackupManager] Invalid backup with no server version - might be a backup created before version 2.0.0`) + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) return res.status(500).send('Invalid backup. Might be a backup created before version 2.0.0.') } + await zip.close().catch(() => {}) + backup.fileSize = await getFileSize(backup.fullPath) const existingBackupIndex = this.backups.findIndex((b) => b.id === backup.id) @@ -257,9 +279,24 @@ class BackupManager { let data = null try { zip = new StreamZip.async({ file: fullFilePath }) + const entries = await zip.entries() + + const detailsEntry = entries['details'] + if (!detailsEntry) { + Logger.error(`[BackupManager] Backup "${fullFilePath}" missing details entry - skipping`) + await zip.close().catch(() => {}) + continue + } + if (detailsEntry.size > 1024 * 1024) { + Logger.error(`[BackupManager] Backup "${fullFilePath}" details entry too large (${detailsEntry.size} bytes) - skipping`) + await zip.close().catch(() => {}) + continue + } + data = await zip.entryData('details') } catch (error) { Logger.error(`[BackupManager] Failed to unzip backup "${fullFilePath}"`, error) + if (zip) await zip.close().catch(() => {}) continue }