CVE-2024-0763 (JavaScript, published 2024-02-27T22:15Z)
Description: value: Any user can delete an arbitrary folder (recursively) on a remote server due to bad input sanitization leading to path traversal. The attacker would need access to the server at some privilege level since this endpoint is protected and requires authorization.
Severity: nan (CVSS v3 base score: )
Weakness: CWE-20 \u2014 Improper Input Validation
The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
Repository: https://github.com/mintplex-labs/anything-llm
Files changed:
server/utils/files/index.js
server/utils/files/purgeDocument.js
Commit message: Employ strict validations on document pathing (#627)

* Employ strict validations on document pathing

* add comment

* update validSubfolder var

Vulnerable code:
  const fullPath =
    process.env.NODE_ENV === "development"
      ? 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");
  const folder =
    process.env.NODE_ENV === "development"
      ? path.resolve(__dirname, `../../storage/documents`)
      : path.resolve(process.env.STORAGE_DIR, `documents`);
  const dirExists = fs.existsSync(folder);
  if (!dirExists) fs.mkdirSync(folder);
  for (const file of fs.readdirSync(folder)) {

    const folderPath =
      process.env.NODE_ENV === "development"
        ? path.resolve(__dirname, `../../storage/documents/${file}`)
        : path.resolve(process.env.STORAGE_DIR, `documents/${file}`);

  const file =
    process.env.NODE_ENV === "development"
      ? path.resolve(__dirname, `../../storage/vector-cache/${digest}.json`)
      : path.resolve(process.env.STORAGE_DIR, `vector-cache/${digest}.json`);
  const folder =
    process.env.NODE_ENV === "development"
      ? path.resolve(__dirname, `../../storage/vector-cache`)
      : path.resolve(process.env.STORAGE_DIR, `vector-cache`);

  if (!fs.existsSync(folder)) fs.mkdirSync(folder);
  const writeTo = path.resolve(folder, `${digest}.json`);
  const filePath =
    process.env.NODE_ENV === "development"
      ? path.resolve(
          __dirname,
          `../../storage/documents`,
          normalizePath(filename)
        )
      : path.resolve(
          process.env.STORAGE_DIR,
          `documents`,
          normalizePath(filename)
        );

  if (!fs.existsSync(filePath)) return;
  console.log(`Purging vector-cache of ${filename}.`);

  const filePath =
    process.env.NODE_ENV === "development"
      ? path.resolve(__dirname, `../../storage/vector-cache`, `${digest}.json`)
      : path.resolve(process.env.STORAGE_DIR, `vector-cache`, `${digest}.json`);
  if (!fs.existsSync(filePath)) return;
  const documentsFolder =
    process.env.NODE_ENV === "development"
      ? path.resolve(__dirname, `../../storage/documents`)
      : path.resolve(process.env.STORAGE_DIR, `documents`);

  for (const folder of fs.readdirSync(documentsFolder)) {
      .lstatSync(path.join(documentsFolder, folder))
    const targetFileLocation = path.join(
      documentsFolder,
      folder,
      targetFilename
    );
    if (!fs.existsSync(targetFileLocation)) continue;
  return path.normalize(filepath).replace(/^(\.\.(\/|\\|$))+/, "");
const { purgeVectorCache, purgeSourceDocument, normalizePath } = require(".");
async function purgeDocument(filename) {
  await purgeVectorCache(filename);
  await purgeSourceDocument(filename);
async function purgeFolder(folderName) {
  if (folderName === "custom-documents") return;
  const documentsFolder =
    process.env.NODE_ENV === "development"
      ? path.resolve(__dirname, `../../storage/documents`)
      : path.resolve(process.env.STORAGE_DIR, `documents`);
  const folderPath = path.resolve(documentsFolder, normalizePath(folderName));
    .readdirSync(folderPath)
    .map((file) => path.join(folderPath, file));
  fs.rmSync(folderPath, { recursive: true }); // Delete root document and source files.

Fixed code:
const documentsPath =
  process.env.NODE_ENV === "development"
    ? path.resolve(__dirname, `../../storage/documents`)
    : path.resolve(process.env.STORAGE_DIR, `documents`);
const vectorCachePath =
  process.env.NODE_ENV === "development"
    ? path.resolve(__dirname, `../../storage/vector-cache`)
    : path.resolve(process.env.STORAGE_DIR, `vector-cache`);
  const fullFilePath = path.resolve(documentsPath, normalizePath(filePath));
  if (!fs.existsSync(fullFilePath) || !isWithin(documentsPath, fullFilePath))
    return null;
  const data = fs.readFileSync(fullFilePath, "utf8");
  if (!fs.existsSync(documentsPath)) fs.mkdirSync(documentsPath);
  for (const file of fs.readdirSync(documentsPath)) {
    const folderPath = path.resolve(documentsPath, file);
  const file = path.resolve(vectorCachePath, `${digest}.json`);
  if (!fs.existsSync(vectorCachePath)) fs.mkdirSync(vectorCachePath);
  const writeTo = path.resolve(vectorCachePath, `${digest}.json`);
  const filePath = path.resolve(documentsPath, normalizePath(filename));

  if (
    !fs.existsSync(filePath) ||
    !isWithin(documentsPath, filePath) ||
    !fs.lstatSync(filePath).isFile()
  )
    return;

  const filePath = path.resolve(vectorCachePath, `${digest}.json`);
  if (!fs.existsSync(filePath) || !fs.lstatSync(filePath).isFile()) return;
  console.log(`Purging vector-cache of ${filename}.`);
  for (const folder of fs.readdirSync(documentsPath)) {
      .lstatSync(path.join(documentsPath, folder))
    const targetFileLocation = path.join(documentsPath, folder, targetFilename);

    if (
      !fs.existsSync(targetFileLocation) ||
      !isWithin(documentsPath, targetFileLocation)
    )
      continue;
/**
 * Checks if a given path is within another path.
 * @param {string} outer - The outer path (should be resolved).
 * @param {string} inner - The inner path (should be resolved).
 * @returns {boolean} - Returns true if the inner path is within the outer path, false otherwise.
 */
function isWithin(outer, inner) {
  if (outer === inner) return false;
  const rel = path.relative(outer, inner);
  return !rel.startsWith("../") && rel !== "..";
}

  const result = path
    .normalize(filepath.trim())
    .replace(/^(\.\.(\/|\\|$))+/, "")
    .trim();
  if (["..", ".", "/"].includes(result)) throw new Error("Invalid path.");
  return result;
  isWithin,
  documentsPath,
const {
  purgeVectorCache,
  purgeSourceDocument,
  normalizePath,
  isWithin,
  documentsPath,
} = require(".");
async function purgeDocument(filename = null) {
  if (!filename || !normalizePath(filename)) return;

  await purgeVectorCache(filename);
  await purgeSourceDocument(filename);
async function purgeFolder(folderName = null) {
  if (!folderName) return;
  const subFolder = normalizePath(folderName);
  const subFolderPath = path.resolve(documentsPath, subFolder);
  const validRemovableSubFolders = fs
    .readdirSync(documentsPath)
    .map((folder) => {
      // Filter out any results which are not folders or
      // are the protected custom-documents folder.
      if (folder === "custom-documents") return null;
      const subfolderPath = path.resolve(documentsPath, folder);
      if (!fs.lstatSync(subfolderPath).isDirectory()) return null;
      return folder;
    })
    .filter((subFolder) => !!subFolder);

  if (
    !validRemovableSubFolders.includes(subFolder) ||
    !fs.existsSync(subFolderPath) ||
    !isWithin(documentsPath, subFolderPath)
  )
    return;
    .readdirSync(subFolderPath)
    .map((file) => path.join(subFolderPath, file));
  fs.rmSync(subFolderPath, { recursive: true }); // Delete target document-folder and source files.