feat: safer backups

This commit is contained in:
Vasily Zubarev
2025-04-04 14:52:48 +02:00
parent 1b1d72b22d
commit 48cb9c50cb
5 changed files with 18 additions and 11 deletions

View File

@@ -10,6 +10,7 @@ import path from "path"
const SUPPORTED_BACKUP_VERSIONS = ["1.0"]
const REMOVE_EXISTING_DATA = true
const MAX_BACKUP_SIZE = 256 * 1024 * 1024 // 256MB
export async function restoreBackupAction(prevState: any, formData: FormData) {
const user = await getCurrentUser()
@@ -20,6 +21,10 @@ export async function restoreBackupAction(prevState: any, formData: FormData) {
return { success: false, error: "No file provided" }
}
if (file.size > MAX_BACKUP_SIZE) {
return { success: false, error: `Backup file too large. Maximum size is ${MAX_BACKUP_SIZE / 1024 / 1024}MB` }
}
// Read zip archive
let zip: JSZip
try {
@@ -88,7 +93,7 @@ export async function restoreBackupAction(prevState: any, formData: FormData) {
const userUploadsDirectory = await getUserUploadsDirectory(user)
for (const file of files) {
const filePathWithoutPrefix = file.path.replace(/^.*\/uploads\//, "")
const filePathWithoutPrefix = path.normalize(file.path.replace(/^.*\/uploads\//, ""))
const zipFilePath = path.join("data/uploads", filePathWithoutPrefix)
const zipFile = zip.file(zipFilePath)
if (!zipFile) {
@@ -96,12 +101,16 @@ export async function restoreBackupAction(prevState: any, formData: FormData) {
continue
}
const fileContents = await zipFile.async("nodebuffer")
const fullFilePath = path.join(userUploadsDirectory, filePathWithoutPrefix)
const fileContent = await zipFile.async("nodebuffer")
if (!fullFilePath.startsWith(path.normalize(userUploadsDirectory))) {
console.error(`Attempted path traversal detected for file ${file.path}`)
continue
}
try {
await fs.mkdir(path.dirname(fullFilePath), { recursive: true })
await fs.writeFile(fullFilePath, fileContent)
await fs.writeFile(fullFilePath, fileContents)
restoredFilesCount++
} catch (error) {
console.error(`Error writing file ${fullFilePath}:`, error)

View File

@@ -81,8 +81,8 @@ export async function saveFileAsTransactionAction(prevState: any, formData: Form
const newRelativeFilePath = await getTransactionFileUploadPath(file.id, originalFileName, transaction)
// Move file to new location and name
const oldFullFilePath = path.join(userUploadsDirectory, file.path)
const newFullFilePath = path.join(userUploadsDirectory, newRelativeFilePath)
const oldFullFilePath = path.join(userUploadsDirectory, path.normalize(file.path))
const newFullFilePath = path.join(userUploadsDirectory, path.normalize(newRelativeFilePath))
await mkdir(path.dirname(newFullFilePath), { recursive: true })
await rename(path.resolve(oldFullFilePath), path.resolve(newFullFilePath))