CVE-2023-5833: Improper Access Control (CWE-284)
value: Improper Access Control in GitHub repository mintplex-labs/anything-llm prior to 0.1.0.
CWE description: The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.
CVSS v3 base score: 8.8
Published: 2023-10-30T13:15Z
Repository: https://github.com/mintplex-labs/anything-llm
Language: JavaScript
Files changed: server/endpoints/system.js, server/utils/helpers/updateENV.js, server/utils/prisma/migrateFromSqlite.js
Commit date: 2023-09-29 10:44:40 -0700
Commit message: Prevent updates of specific keys via API (#256)

prevent non-admin from updating settings
prevent password updates in multi-user mdoe
Diff: diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index 1717e63..89896aa 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -237,10 +237,20 @@ function systemEndpoints(app) {
     "/system/update-env",
     [validatedRequest],
     async (request, response) => {
       try {
         const body = reqBody(request);
+
+        // Only admins can update the ENV settings.
+        if (multiUserMode(response)) {
+          const user = await userFromSession(request, response);
+          if (!user || user?.role !== "admin") {
+            response.sendStatus(401).end();
+            return;
+          }
+        }
+
         const { newValues, error } = updateENV(body);
         if (process.env.NODE_ENV === "production") await dumpENV();
         response.status(200).json({ newValues, error });
       } catch (e) {
         console.log(e.message, e);
@@ -252,15 +262,25 @@ function systemEndpoints(app) {
   app.post(
     "/system/update-password",
     [validatedRequest],
     async (request, response) => {
       try {
+        // Cannot update password in multi - user mode.
+        if (multiUserMode(response)) {
+          response.sendStatus(401).end();
+          return;
+        }
+
         const { usePassword, newPassword } = reqBody(request);
-        const { error } = updateENV({
-          AuthToken: usePassword ? newPassword : "",
-          JWTSecret: usePassword ? v4() : "",
-        });
+        const { error } = updateENV(
+          {
+            AuthToken: usePassword ? newPassword : "",
+            JWTSecret: usePassword ? v4() : "",
+          },
+          true
+        );
+        if (process.env.NODE_ENV === "production") await dumpENV();
         response.status(200).json({ success: !error, error });
       } catch (e) {
         console.log(e.message, e);
         response.sendStatus(500).end();
       }
@@ -291,12 +311,19 @@ function systemEndpoints(app) {
           multi_user_mode: true,
           users_can_delete_workspaces: false,
           limit_user_messages: false,
           message_limit: 25,
         });
-        process.env.AUTH_TOKEN = null;
-        process.env.JWT_SECRET = process.env.JWT_SECRET ?? v4(); // Make sure JWT_SECRET is set for JWT issuance.
+
+        updateENV(
+          {
+            AuthToken: null,
+            JWTSecret: process.env.JWT_SECRET ?? v4(),
+          },
+          true
+        );
+        if (process.env.NODE_ENV === "production") await dumpENV();
         await Telemetry.sendTelemetry("enabled_multi_user_mode");
         response.status(200).json({ success: !!user, error });
       } catch (e) {
         console.log(e.message, e);
         response.sendStatus(500).end();
diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js
index 2abff32..78561cb 100644
--- a/server/utils/helpers/updateENV.js
+++ b/server/utils/helpers/updateENV.js
@@ -70,15 +70,15 @@ const KEY_MAPPING = {
   },
 
   // System Settings
   AuthToken: {
     envKey: "AUTH_TOKEN",
-    checks: [],
+    checks: [requiresForceMode],
   },
   JWTSecret: {
     envKey: "JWT_SECRET",
-    checks: [],
+    checks: [requiresForceMode],
   },
   // Not supported yet.
   // 'StorageDir': 'STORAGE_DIR',
 };
 
@@ -141,15 +141,19 @@ function validAzureURL(input = "") {
   } catch {
     return "Not a valid URL";
   }
 }
 
+function requiresForceMode(_, forceModeEnabled = false) {
+  return forceModeEnabled === true ? null : "Cannot set this setting.";
+}
+
 // This will force update .env variables which for any which reason were not able to be parsed or
 // read from an ENV file as this seems to be a complicating step for many so allowing people to write
 // to the process will at least alleviate that issue. It does not perform comprehensive validity checks or sanity checks
 // and is simply for debugging when the .env not found issue many come across.
-function updateENV(newENVs = {}) {
+function updateENV(newENVs = {}, force = false) {
   let error = "";
   const validKeys = Object.keys(KEY_MAPPING);
   const ENV_KEYS = Object.keys(newENVs).filter(
     (key) => validKeys.includes(key) && !newENVs[key].includes("******") // strip out answers where the value is all asterisks
   );
@@ -157,11 +161,11 @@ function updateENV(newENVs = {}) {
 
   ENV_KEYS.forEach((key) => {
     const { envKey, checks } = KEY_MAPPING[key];
     const value = newENVs[key];
     const errors = checks
-      .map((validityCheck) => validityCheck(value))
+      .map((validityCheck) => validityCheck(value, force))
       .filter((err) => typeof err === "string");
 
     if (errors.length > 0) {
       error += errors.join("\n");
       return;
diff --git a/server/utils/prisma/migrateFromSqlite.js b/server/utils/prisma/migrateFromSqlite.js
index c874aef..4e5e379 100644
--- a/server/utils/prisma/migrateFromSqlite.js
+++ b/server/utils/prisma/migrateFromSqlite.js
@@ -251,11 +251,11 @@ async function migrateTable(tableName, migrateRowFunc) {
   });
 
   // Check table exists
   const { count } = await db.get(
     `SELECT COUNT(*) as count FROM sqlite_master WHERE name='${tableName}'`
-  )
+  );
   if (count === 0) {
     console.log(
       `${tableName} does not exist in legacy DB - nothing to migrate - skipping.`
     );
     return;

Vulnerable code:         const { error } = updateENV({
          AuthToken: usePassword ? newPassword : "",
          JWTSecret: usePassword ? v4() : "",
        });
        process.env.AUTH_TOKEN = null;
        process.env.JWT_SECRET = process.env.JWT_SECRET ?? v4(); // Make sure JWT_SECRET is set for JWT issuance.
    checks: [],
    checks: [],
function updateENV(newENVs = {}) {
      .map((validityCheck) => validityCheck(value))
  )
Fixed code: 
        // Only admins can update the ENV settings.
        if (multiUserMode(response)) {
          const user = await userFromSession(request, response);
          if (!user || user?.role !== "admin") {
            response.sendStatus(401).end();
            return;
          }
        }

        // Cannot update password in multi - user mode.
        if (multiUserMode(response)) {
          response.sendStatus(401).end();
          return;
        }

        const { error } = updateENV(
          {
            AuthToken: usePassword ? newPassword : "",
            JWTSecret: usePassword ? v4() : "",
          },
          true
        );
        if (process.env.NODE_ENV === "production") await dumpENV();

        updateENV(
          {
            AuthToken: null,
            JWTSecret: process.env.JWT_SECRET ?? v4(),
          },
          true
        );
        if (process.env.NODE_ENV === "production") await dumpENV();
    checks: [requiresForceMode],
    checks: [requiresForceMode],
function requiresForceMode(_, forceModeEnabled = false) {
  return forceModeEnabled === true ? null : "Cannot set this setting.";
}

function updateENV(newENVs = {}, force = false) {
      .map((validityCheck) => validityCheck(value, force))
  );