diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue index 2c9442ca..eef05b60 100644 --- a/client/pages/upload/index.vue +++ b/client/pages/upload/index.vue @@ -359,15 +359,14 @@ export default { // Check if path already exists before starting upload // uploading fails if path already exists for (const item of items) { - const filepath = Path.join(this.selectedFolder.fullPath, item.directory) const exists = await this.$axios - .$post(`/api/filesystem/pathexists`, { filepath, directory: item.directory, folderPath: this.selectedFolder.fullPath }) + .$post(`/api/filesystem/pathexists`, { directory: item.directory, folderPath: this.selectedFolder.fullPath }) .then((data) => { if (data.exists) { if (data.libraryItemTitle) { this.$toast.error(this.$getString('ToastUploaderItemExistsInSubdirectoryError', [data.libraryItemTitle])) } else { - this.$toast.error(this.$getString('ToastUploaderFilepathExistsError', [filepath])) + this.$toast.error(this.$getString('ToastUploaderFilepathExistsError', [Path.join(this.selectedFolder.fullPath, item.directory)])) } } return data.exists diff --git a/server/controllers/FileSystemController.js b/server/controllers/FileSystemController.js index d0b190a4..7629f9ee 100644 --- a/server/controllers/FileSystemController.js +++ b/server/controllers/FileSystemController.js @@ -84,49 +84,67 @@ class FileSystemController { */ async checkPathExists(req, res) { if (!req.user.canUpload) { - Logger.error(`[FileSystemController] Non-admin user "${req.user.username}" attempting to check path exists`) + Logger.error(`[FileSystemController] User "${req.user.username}" without upload permissions attempting to check path exists`) return res.sendStatus(403) } - const { filepath, directory, folderPath } = req.body + const { directory, folderPath } = req.body - if (!filepath?.length || typeof filepath !== 'string') { + if (!directory?.length || typeof directory !== 'string' || !folderPath?.length || typeof folderPath !== 'string') { + Logger.error(`[FileSystemController] Invalid request body: ${JSON.stringify(req.body)}`) + return res.status(400).json({ + error: 'Invalid request body' + }) + } + + // Check that library folder exists + const libraryFolder = await Database.libraryFolderModel.findOne({ + where: { + path: folderPath + } + }) + + if (!libraryFolder) { + Logger.error(`[FileSystemController] Library folder not found: ${folderPath}`) + return res.sendStatus(404) + } + + const filepath = Path.posix.join(libraryFolder.path, directory) + // Ensure filepath is inside library folder (prevents directory traversal) + if (!filepath.startsWith(libraryFolder.path)) { + Logger.error(`[FileSystemController] Filepath is not inside library folder: ${filepath}`) return res.sendStatus(400) } - const exists = await fs.pathExists(filepath) - - if (exists) { + if (await fs.pathExists(filepath)) { return res.json({ exists: true }) } - // If directory and folderPath are passed in, check if a library item exists in a subdirectory + // Check if a library item exists in a subdirectory // See: https://github.com/advplyr/audiobookshelf/issues/4146 - if (typeof directory === 'string' && typeof folderPath === 'string' && directory.length > 0 && folderPath.length > 0) { - const cleanedDirectory = directory.split('/').filter(Boolean).join('/') - if (cleanedDirectory.includes('/')) { - // Can only be 2 levels deep - const possiblePaths = [] - const subdir = Path.dirname(directory) - possiblePaths.push(fileUtils.filePathToPOSIX(Path.join(folderPath, subdir))) - if (subdir.includes('/')) { - possiblePaths.push(fileUtils.filePathToPOSIX(Path.join(folderPath, Path.dirname(subdir)))) - } + const cleanedDirectory = directory.split('/').filter(Boolean).join('/') + if (cleanedDirectory.includes('/')) { + // Can only be 2 levels deep + const possiblePaths = [] + const subdir = Path.dirname(directory) + possiblePaths.push(fileUtils.filePathToPOSIX(Path.join(folderPath, subdir))) + if (subdir.includes('/')) { + possiblePaths.push(fileUtils.filePathToPOSIX(Path.join(folderPath, Path.dirname(subdir)))) + } - const libraryItem = await Database.libraryItemModel.findOne({ - where: { - path: possiblePaths - } + const libraryItem = await Database.libraryItemModel.findOne({ + where: { + path: possiblePaths + } + }) + + if (libraryItem) { + return res.json({ + exists: true, + libraryItemTitle: libraryItem.title }) - - if (libraryItem) { - return res.json({ - exists: true, - libraryItemTitle: libraryItem.title - }) - } } }