From e775564fc1e728504c008c314aa98ee28e91caae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 10 Mar 2025 17:59:31 -0400 Subject: [PATCH] refactor: prevent following symlinks --- src/controllers/admin/uploads.js | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index bb777c2f31..ced7385983 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -26,13 +26,13 @@ uploadsController.get = async function (req, res, next) { const page = parseInt(req.query.page, 10) || 1; let files = []; try { - files = await fs.promises.readdir(currentFolder); + await checkSymLinks(req.query.dir) + files = await getFilesInFolder(currentFolder); } catch (err) { winston.error(err.stack); return next(new Error('[[error:invalid-path]]')); } try { - files = files.filter(filename => filename !== '.gitignore'); const itemCount = files.length; const start = Math.max(0, (page - 1) * itemsPerPage); const stop = start + itemsPerPage; @@ -72,6 +72,30 @@ uploadsController.get = async function (req, res, next) { } }; +async function checkSymLinks(folder) { + let dir = path.normalize(folder || ''); + while (dir.length && dir !== '.') { + const nextPath = path.join(nconf.get('upload_path'), dir); + // eslint-disable-next-line no-await-in-loop + const stat = await fs.promises.lstat(nextPath); + if (stat.isSymbolicLink()) { + throw new Error('[[invalid-path]]'); + } + dir = path.dirname(dir); + } +} + +async function getFilesInFolder(folder) { + const dirents = await fs.promises.readdir(folder, { withFileTypes: true }); + const files = []; + for await (const dirent of dirents) { + if (!dirent.isSymbolicLink() && dirent.name !== '.gitignore') { + files.push(dirent.name); + } + } + return files; +} + function buildBreadcrumbs(currentFolder) { const crumbs = []; const parts = currentFolder.replace(nconf.get('upload_path'), '').split(path.sep); @@ -102,14 +126,14 @@ async function getFileData(currentDir, file) { const stat = await fs.promises.stat(pathToFile); let filesInDir = []; if (stat.isDirectory()) { - filesInDir = await fs.promises.readdir(pathToFile); + filesInDir = await getFilesInFolder(pathToFile); } const url = `${nconf.get('upload_url') + currentDir.replace(nconf.get('upload_path'), '')}/${file}`; return { name: file, path: pathToFile.replace(path.join(nconf.get('upload_path'), '/'), ''), url: url, - fileCount: Math.max(0, filesInDir.length - 1), // ignore .gitignore + fileCount: filesInDir.length, size: stat.size, sizeHumanReadable: `${(stat.size / 1024).toFixed(1)}KiB`, isDirectory: stat.isDirectory(),