CVE-2022-36065 (published 2022-09-06T21:15Z)

GrowthBook is an open-source platform for feature flagging and A/B testing. With some self-hosted configurations in versions prior to 2022-08-29, attackers can register new accounts and upload files to arbitrary directories within the container. If the attacker uploads a Python script to the right location, they can execute arbitrary code within the container. To be affected, ALL of the following must be true: Self-hosted deployment (GrowthBook Cloud is unaffected); using local file uploads (as opposed to S3 or Google Cloud Storage); NODE_ENV set to a non-production value and JWT_SECRET set to an easily guessable string like `dev`. This issue is patched in commit 1a5edff8786d141161bf880c2fd9ccbe2850a264 (2022-08-29). As a workaround, set `JWT_SECRET` environment variable to a long random string. This will stop arbitrary file uploads, but the only way to stop attackers from registering accounts is by updating to the latest build.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/growthbook/growthbook
commit_details: repo_url: https://github.com/growthbook/growthbook
author: Jeremy Dorn
author_date: 2022-08-29 16:30:29-05:00
msg: Secure file uploads when NODE_ENV=dev (#487)
repository: repo_url: https://github.com/growthbook/growthbook
repo_name: growthbook/growthbook
description: Open Source Feature Flagging and A/B Testing Platform
repo_language: TypeScript
owner: growthbook
file_changes: filename: auth.ts
change_type: ModificationType.MODIFY
diff: @@ -183,6 +183,14 @@ export async function postFirstTimeRegister(
   >,
   res: Response
 ) {
+  // Only allow this API endpoint when it's a brand-new installation with no users yet
+  const newInstallation = await isNewInstallation();
+  if (!newInstallation) {
+    throw new Error(
+      "An organization is already configured. Please refresh the page and try again."
+    );
+  }
+
   const { email, name, password, companyname } = req.body;
 
   validatePasswordFormat(password);

programming_language: TypeScript
filename: files.ts
change_type: ModificationType.MODIFY
diff: @@ -42,7 +42,21 @@ export async function uploadFile(
     throw new Error("Invalid upload signature");
   }
 
-  const fullPath = getUploadsDir() + "/" + filePath;
+  // Watch out for poison null bytes
+  if (filePath.indexOf("\0") !== -1) {
+    throw new Error("Error: Filename must not contain null bytes");
+  }
+
+  const rootDirectory = getUploadsDir();
+  const fullPath = path.join(rootDirectory, filePath);
+
+  // Prevent directory traversal
+  if (fullPath.indexOf(rootDirectory) !== 0) {
+    throw new Error(
+      "Error: Path must not escape out of the 'uploads' directory."
+    );
+  }
+
   const dir = path.dirname(fullPath);
   await fs.promises.mkdir(dir, { recursive: true });
   await fs.promises.writeFile(fullPath, contents);

programming_language: TypeScript
filename: secrets.ts
change_type: ModificationType.MODIFY
diff: @@ -29,6 +29,7 @@ if (!MONGODB_URI) {
 }
 
 export const APP_ORIGIN = process.env.APP_ORIGIN || "http://localhost:3000";
+const isLocalhost = APP_ORIGIN.includes("localhost");
 
 const corsOriginRegex = process.env.CORS_ORIGIN_REGEX;
 export const CORS_ORIGIN_REGEX = corsOriginRegex
@@ -56,7 +57,7 @@ export const GCS_DOMAIN =
   `https://storage.googleapis.com/${GCS_BUCKET_NAME}/`;
 
 export const JWT_SECRET = process.env.JWT_SECRET || "dev";
-if (prod && !IS_CLOUD && JWT_SECRET === "dev") {
+if ((prod || !isLocalhost) && !IS_CLOUD && JWT_SECRET === "dev") {
   throw new Error(
     "Cannot use JWT_SECRET=dev in production. Please set to a long random string."
   );

programming_language: TypeScript