CVE-2024-39309: Insufficient Information (NVD-CWE-noinfo)
value: Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. A vulnerability in versions prior to 6.5.7 and 7.1.0 allows SQL injection when Parse Server is configured to use the PostgreSQL database. The algorithm to detect SQL injection has been improved in versions 6.5.7 and 7.1.0. No known workarounds are available.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-07-01T22:15Z
Repository: https://github.com/parse-community/parse-server
Language: JSON
Files changed: package.json, src/Adapters/Storage/Postgres/PostgresStorageAdapter.js
Commit date: 2024-06-30 03:34:08 +0200
Commit message: fix: SQL injection when using Parse Server with PostgreSQL; fixes security vulnerability [GHSA-c2hr-cqg6-8j6r](https://github.com/parse-community/parse-server/security/advisories/GHSA-c2hr-cqg6-8j6r) (#9167)


Diff: diff --git a/package.json b/package.json
index afa387dc6f..bd9e90a1fc 100644
--- a/package.json
+++ b/package.json
@@ -123,10 +123,11 @@
     "test:mongodb:4.2.19": "npm run test:mongodb --dbversion=4.2.19",
     "test:mongodb:4.4.13": "npm run test:mongodb --dbversion=4.4.13",
     "test:mongodb:5.3.2": "npm run test:mongodb --dbversion=5.3.2",
     "test:mongodb:6.0.2": "npm run test:mongodb --dbversion=6.0.2",
     "test:mongodb:7.0.1": "npm run test:mongodb --dbversion=7.0.1",
+    "test:postgres:testonly": "cross-env PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly",
     "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017",
     "testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
     "test": "npm run testonly",
     "posttest": "cross-env mongodb-runner stop --all",
     "coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine",
diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js
index efbe985bf9..cae66fb51a 100644
--- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js
+++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js
@@ -2612,54 +2612,58 @@ function isAnyValueRegexStartsWith(values) {
   return values.some(function (value) {
     return isStartsWithRegex(value.$regex);
   });
 }
 
-function createLiteralRegex(remaining) {
+function createLiteralRegex(remaining: string) {
   return remaining
     .split('')
     .map(c => {
-      const regex = RegExp('[0-9 ]|\\p{L}', 'u'); // Support all unicode letter chars
+      const regex = RegExp('[0-9 ]|\\p{L}', 'u'); // Support all Unicode letter chars
       if (c.match(regex) !== null) {
-        // don't escape alphanumeric characters
+        // Don't escape alphanumeric characters
         return c;
       }
-      // escape everything else (single quotes with single quotes, everything else with a backslash)
+      // Escape everything else (single quotes with single quotes, everything else with a backslash)
       return c === `'` ? `''` : `\\${c}`;
     })
     .join('');
 }
 
 function literalizeRegexPart(s: string) {
   const matcher1 = /\\Q((?!\\E).*)\\E$/;
   const result1: any = s.match(matcher1);
   if (result1 && result1.length > 1 && result1.index > -1) {
-    // process regex that has a beginning and an end specified for the literal text
+    // Process Regex that has a beginning and an end specified for the literal text
     const prefix = s.substring(0, result1.index);
     const remaining = result1[1];
 
     return literalizeRegexPart(prefix) + createLiteralRegex(remaining);
   }
 
-  // process regex that has a beginning specified for the literal text
+  // Process Regex that has a beginning specified for the literal text
   const matcher2 = /\\Q((?!\\E).*)$/;
   const result2: any = s.match(matcher2);
   if (result2 && result2.length > 1 && result2.index > -1) {
     const prefix = s.substring(0, result2.index);
     const remaining = result2[1];
 
     return literalizeRegexPart(prefix) + createLiteralRegex(remaining);
   }
 
-  // remove all instances of \Q and \E from the remaining text & escape single quotes
+  // Remove problematic chars from remaining text
   return s
+    // Remove all instances of \Q and \E
     .replace(/([^\\])(\\E)/, '$1')
     .replace(/([^\\])(\\Q)/, '$1')
     .replace(/^\\E/, '')
     .replace(/^\\Q/, '')
-    .replace(/([^'])'/g, `$1''`)
-    .replace(/^'([^'])/, `''$1`);
+    // Ensure even number of single quote sequences by adding an extra single quote if needed;
+    // this ensures that every single quote is escaped
+    .replace(/'+/g, match => {
+      return match.length % 2 === 0 ? match : match + "'";
+    });
 }
 
 var GeoPointCoder = {
   isValidJSON(value) {
     return typeof value === 'object' && value !== null && value.__type === 'GeoPoint';

Vulnerable code: function createLiteralRegex(remaining) {
      const regex = RegExp('[0-9 ]|\\p{L}', 'u'); // Support all unicode letter chars
        // don't escape alphanumeric characters
      // escape everything else (single quotes with single quotes, everything else with a backslash)
    // process regex that has a beginning and an end specified for the literal text
  // process regex that has a beginning specified for the literal text
  // remove all instances of \Q and \E from the remaining text & escape single quotes
    .replace(/([^'])'/g, `$1''`)
    .replace(/^'([^'])/, `''$1`);
Fixed code:     "test:postgres:testonly": "cross-env PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly",
function createLiteralRegex(remaining: string) {
      const regex = RegExp('[0-9 ]|\\p{L}', 'u'); // Support all Unicode letter chars
        // Don't escape alphanumeric characters
      // Escape everything else (single quotes with single quotes, everything else with a backslash)
    // Process Regex that has a beginning and an end specified for the literal text
  // Process Regex that has a beginning specified for the literal text
  // Remove problematic chars from remaining text
    // Remove all instances of \Q and \E
    // Ensure even number of single quote sequences by adding an extra single quote if needed;
    // this ensures that every single quote is escaped
    .replace(/'+/g, match => {
      return match.length % 2 === 0 ? match : match + "'";
    });