Skip to content

Commit 48cb9c5

Browse files
committed
feat: safer backups
1 parent 1b1d72b commit 48cb9c5

File tree

5 files changed

+18
-11
lines changed

5 files changed

+18
-11
lines changed

app/(app)/settings/backups/actions.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import path from "path"
1010

1111
const SUPPORTED_BACKUP_VERSIONS = ["1.0"]
1212
const REMOVE_EXISTING_DATA = true
13+
const MAX_BACKUP_SIZE = 256 * 1024 * 1024 // 256MB
1314

1415
export async function restoreBackupAction(prevState: any, formData: FormData) {
1516
const user = await getCurrentUser()
@@ -20,6 +21,10 @@ export async function restoreBackupAction(prevState: any, formData: FormData) {
2021
return { success: false, error: "No file provided" }
2122
}
2223

24+
if (file.size > MAX_BACKUP_SIZE) {
25+
return { success: false, error: `Backup file too large. Maximum size is ${MAX_BACKUP_SIZE / 1024 / 1024}MB` }
26+
}
27+
2328
// Read zip archive
2429
let zip: JSZip
2530
try {
@@ -88,20 +93,24 @@ export async function restoreBackupAction(prevState: any, formData: FormData) {
8893
const userUploadsDirectory = await getUserUploadsDirectory(user)
8994

9095
for (const file of files) {
91-
const filePathWithoutPrefix = file.path.replace(/^.*\/uploads\//, "")
96+
const filePathWithoutPrefix = path.normalize(file.path.replace(/^.*\/uploads\//, ""))
9297
const zipFilePath = path.join("data/uploads", filePathWithoutPrefix)
9398
const zipFile = zip.file(zipFilePath)
9499
if (!zipFile) {
95100
console.log(`File ${file.path} not found in backup`)
96101
continue
97102
}
98103

104+
const fileContents = await zipFile.async("nodebuffer")
99105
const fullFilePath = path.join(userUploadsDirectory, filePathWithoutPrefix)
100-
const fileContent = await zipFile.async("nodebuffer")
106+
if (!fullFilePath.startsWith(path.normalize(userUploadsDirectory))) {
107+
console.error(`Attempted path traversal detected for file ${file.path}`)
108+
continue
109+
}
101110

102111
try {
103112
await fs.mkdir(path.dirname(fullFilePath), { recursive: true })
104-
await fs.writeFile(fullFilePath, fileContent)
113+
await fs.writeFile(fullFilePath, fileContents)
105114
restoredFilesCount++
106115
} catch (error) {
107116
console.error(`Error writing file ${fullFilePath}:`, error)

app/(app)/unsorted/actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ export async function saveFileAsTransactionAction(prevState: any, formData: Form
8181
const newRelativeFilePath = await getTransactionFileUploadPath(file.id, originalFileName, transaction)
8282

8383
// Move file to new location and name
84-
const oldFullFilePath = path.join(userUploadsDirectory, file.path)
85-
const newFullFilePath = path.join(userUploadsDirectory, newRelativeFilePath)
84+
const oldFullFilePath = path.join(userUploadsDirectory, path.normalize(file.path))
85+
const newFullFilePath = path.join(userUploadsDirectory, path.normalize(newRelativeFilePath))
8686
await mkdir(path.dirname(newFullFilePath), { recursive: true })
8787
await rename(path.resolve(oldFullFilePath), path.resolve(newFullFilePath))
8888

components/forms/convert-currency.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,7 @@ export const FormConvertCurrency = ({
7878
async function getCurrencyRate(currencyCodeFrom: string, currencyCodeTo: string, date: Date): Promise<number> {
7979
try {
8080
const formattedDate = format(date, "yyyy-MM-dd")
81-
const url = `/api/currency?from=${currencyCodeFrom}&to=${currencyCodeTo}&date=${formattedDate}`
82-
83-
const response = await fetch(url)
81+
const response = await fetch(`/api/currency?from=${currencyCodeFrom}&to=${currencyCodeTo}&date=${formattedDate}`)
8482

8583
if (!response.ok) {
8684
const errorData = await response.json()

lib/files.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export async function getTransactionFileUploadPath(fileUuid: string, filename: s
3232

3333
export async function fullPathForFile(user: User, file: File) {
3434
const userUploadsDirectory = await getUserUploadsDirectory(user)
35-
return path.join(userUploadsDirectory, file.path)
35+
return path.join(userUploadsDirectory, path.normalize(file.path))
3636
}
3737

3838
function formatFilePath(filename: string, date: Date, format = "{YYYY}/{MM}/{name}{ext}") {
@@ -46,7 +46,7 @@ function formatFilePath(filename: string, date: Date, format = "{YYYY}/{MM}/{nam
4646

4747
export async function fileExists(filePath: string) {
4848
try {
49-
await access(filePath, constants.F_OK)
49+
await access(path.normalize(filePath), constants.F_OK)
5050
return true
5151
} catch {
5252
return false

models/files.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const deleteFile = async (id: string, userId: string) => {
7474
}
7575

7676
try {
77-
await unlink(path.resolve(file.path))
77+
await unlink(path.resolve(path.normalize(file.path)))
7878
} catch (error) {
7979
console.error("Error deleting file:", error)
8080
}

0 commit comments

Comments
 (0)