CVE-2024-0549: Relative Path Traversal (CWE-23)
value: mintplex-labs/anything-llm is vulnerable to a relative path traversal attack, allowing unauthorized attackers with a default role account to delete files and folders within the filesystem, including critical database files such as 'anythingllm.db'. The vulnerability stems from insufficient input validation and normalization in the handling of file and folder deletion requests. Successful exploitation results in the compromise of data integrity and availability.
CWE description: The product uses external input to construct a pathname that should be within a restricted directory, but it does not properly neutralize sequences such as ".." that can resolve to a location that is outside of that directory.
Published: 2024-04-16T00:15Z
Repository: https://github.com/mintplex-labs/anything-llm
Language: JavaScript
Files changed: server/endpoints/system.js, server/utils/files/index.js, server/utils/files/purgeDocument.js
Commit date: 2024-01-14 16:36:17 -0800
Commit message: normalize paths for submit URLs of  `remove-documents (#598)

normalize paths for submit URLs
Diff: diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index 345bd230..6d985065 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -1,9 +1,9 @@
 process.env.NODE_ENV === "development"
   ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` })
   : require("dotenv").config();
-const { viewLocalFiles } = require("../utils/files");
+const { viewLocalFiles, normalizePath } = require("../utils/files");
 const { exportData, unpackAndOverwriteImport } = require("../utils/files/data");
 const {
   checkProcessorAlive,
   acceptedFileTypes,
 } = require("../utils/files/documentProcessor");
@@ -399,13 +399,11 @@ function systemEndpoints(app) {
     }
   });
 
   app.get("/system/data-exports/:filename", (request, response) => {
     const exportLocation = __dirname + "/../storage/exports/";
-    const sanitized = path
-      .normalize(request.params.filename)
-      .replace(/^(\.\.(\/|\\|$))+/, "");
+    const sanitized = normalizePath(request.params.filename);
     const finalDestination = path.join(exportLocation, sanitized);
 
     if (!fs.existsSync(finalDestination)) {
       response.status(404).json({
         error: 404,
diff --git a/server/utils/files/index.js b/server/utils/files/index.js
index b6c7a307..2ff1d60c 100644
--- a/server/utils/files/index.js
+++ b/server/utils/files/index.js
@@ -1,44 +1,25 @@
 const fs = require("fs");
 const path = require("path");
 const { v5: uuidv5 } = require("uuid");
 
-async function collectDocumentData(folderName = null) {
-  if (!folderName) throw new Error("No docPath provided in request");
-  const folder =
-    process.env.NODE_ENV === "development"
-      ? path.resolve(__dirname, `../../storage/documents/${folderName}`)
-      : path.resolve(process.env.STORAGE_DIR, `documents/${folderName}`);
-
-  const dirExists = fs.existsSync(folder);
-  if (!dirExists)
-    throw new Error(
-      `No documents folder for ${folderName} - did you run collector/main.py for this element?`
-    );
-
-  const files = fs.readdirSync(folder);
-  const fileData = [];
-  files.forEach((file) => {
-    if (path.extname(file) === ".json") {
-      const filePath = path.join(folder, file);
-      const data = fs.readFileSync(filePath, "utf8");
-      console.log(`Parsing document: ${file}`);
-      fileData.push(JSON.parse(data));
-    }
-  });
-  return fileData;
-}
-
 // Should take in a folder that is a subfolder of documents
 // eg: youtube-subject/video-123.json
 async function fileData(filePath = null) {
   if (!filePath) throw new Error("No docPath provided in request");
 
   const fullPath =
     process.env.NODE_ENV === "development"
-      ? path.resolve(__dirname, `../../storage/documents/${filePath}`)
-      : path.resolve(process.env.STORAGE_DIR, `documents/${filePath}`);
+      ? path.resolve(
+          __dirname,
+          `../../storage/documents/${normalizePath(filePath)}`
+        )
+      : path.resolve(
+          process.env.STORAGE_DIR,
+          `documents/${normalizePath(filePath)}`
+        );
+
   const fileExists = fs.existsSync(fullPath);
   if (!fileExists) return null;
 
   const data = fs.readFileSync(fullPath, "utf8");
   return JSON.parse(data);
@@ -140,15 +121,22 @@ async function storeVectorResult(vectorData = [], filename = null) {
 
 // Purges a file from the documents/ folder.
 async function purgeSourceDocument(filename = null) {
   if (!filename) return;
   console.log(`Purging source document of ${filename}.`);
-
   const filePath =
     process.env.NODE_ENV === "development"
-      ? path.resolve(__dirname, `../../storage/documents`, filename)
-      : path.resolve(process.env.STORAGE_DIR, `documents`, filename);
+      ? path.resolve(
+          __dirname,
+          `../../storage/documents`,
+          normalizePath(filename)
+        )
+      : path.resolve(
+          process.env.STORAGE_DIR,
+          `documents`,
+          normalizePath(filename)
+        );
 
   if (!fs.existsSync(filePath)) return;
   fs.rmSync(filePath);
   return;
 }
@@ -167,14 +155,18 @@ async function purgeVectorCache(filename = null) {
   if (!fs.existsSync(filePath)) return;
   fs.rmSync(filePath);
   return;
 }
 
+function normalizePath(filepath = "") {
+  return path.normalize(filepath).replace(/^(\.\.(\/|\\|$))+/, "");
+}
+
 module.exports = {
   cachedVectorInformation,
-  collectDocumentData,
   viewLocalFiles,
   purgeSourceDocument,
   purgeVectorCache,
   storeVectorResult,
   fileData,
+  normalizePath,
 };
diff --git a/server/utils/files/purgeDocument.js b/server/utils/files/purgeDocument.js
index 27fe1471..46e9d37d 100644
--- a/server/utils/files/purgeDocument.js
+++ b/server/utils/files/purgeDocument.js
@@ -1,9 +1,8 @@
 const fs = require("fs");
 const path = require("path");
-
-const { purgeVectorCache, purgeSourceDocument } = require(".");
+const { purgeVectorCache, purgeSourceDocument, normalizePath } = require(".");
 const { Document } = require("../../models/documents");
 const { Workspace } = require("../../models/workspace");
 
 async function purgeDocument(filename) {
   const workspaces = await Workspace.where();
@@ -20,14 +19,14 @@ async function purgeFolder(folderName) {
   const documentsFolder =
     process.env.NODE_ENV === "development"
       ? path.resolve(__dirname, `../../storage/documents`)
       : path.resolve(process.env.STORAGE_DIR, `documents`);
 
-  const folderPath = path.resolve(documentsFolder, folderName);
+  const folderPath = path.resolve(documentsFolder, normalizePath(folderName));
   const filenames = fs
     .readdirSync(folderPath)
-    .map((file) => path.join(folderName, file));
+    .map((file) => path.join(folderPath, file));
   const workspaces = await Workspace.where();
 
   const purgePromises = [];
   // Remove associated Vector-cache files
   for (const filename of filenames) {

Vulnerable code: const { viewLocalFiles } = require("../utils/files");
    const sanitized = path
      .normalize(request.params.filename)
      .replace(/^(\.\.(\/|\\|$))+/, "");
async function collectDocumentData(folderName = null) {
  if (!folderName) throw new Error("No docPath provided in request");
  const folder =
    process.env.NODE_ENV === "development"
      ? path.resolve(__dirname, `../../storage/documents/${folderName}`)
      : path.resolve(process.env.STORAGE_DIR, `documents/${folderName}`);

  const dirExists = fs.existsSync(folder);
  if (!dirExists)
    throw new Error(
      `No documents folder for ${folderName} - did you run collector/main.py for this element?`
    );

  const files = fs.readdirSync(folder);
  const fileData = [];
  files.forEach((file) => {
    if (path.extname(file) === ".json") {
      const filePath = path.join(folder, file);
      const data = fs.readFileSync(filePath, "utf8");
      console.log(`Parsing document: ${file}`);
      fileData.push(JSON.parse(data));
    }
  });
  return fileData;
}

      ? path.resolve(__dirname, `../../storage/documents/${filePath}`)
      : path.resolve(process.env.STORAGE_DIR, `documents/${filePath}`);

      ? path.resolve(__dirname, `../../storage/documents`, filename)
      : path.resolve(process.env.STORAGE_DIR, `documents`, filename);
  collectDocumentData,

const { purgeVectorCache, purgeSourceDocument } = require(".");
  const folderPath = path.resolve(documentsFolder, folderName);
    .map((file) => path.join(folderName, file));
Fixed code: const { viewLocalFiles, normalizePath } = require("../utils/files");
    const sanitized = normalizePath(request.params.filename);
      ? path.resolve(
          __dirname,
          `../../storage/documents/${normalizePath(filePath)}`
        )
      : path.resolve(
          process.env.STORAGE_DIR,
          `documents/${normalizePath(filePath)}`
        );

      ? path.resolve(
          __dirname,
          `../../storage/documents`,
          normalizePath(filename)
        )
      : path.resolve(
          process.env.STORAGE_DIR,
          `documents`,
          normalizePath(filename)
        );
function normalizePath(filepath = "") {
  return path.normalize(filepath).replace(/^(\.\.(\/|\\|$))+/, "");
}

  normalizePath,
const { purgeVectorCache, purgeSourceDocument, normalizePath } = require(".");
  const folderPath = path.resolve(documentsFolder, normalizePath(folderName));
    .map((file) => path.join(folderPath, file));