From 2edf1e4c0363af01e97a7fbc97694f851b7d1ff3 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 30 Jun 2024 03:34:08 +0200 Subject: [PATCH] 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) --- package.json | 1 + .../Postgres/PostgresStorageAdapter.js | 22 +++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index afa387dc6f..bd9e90a1fc 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "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", 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 @@ -2614,16 +2614,16 @@ function isAnyValueRegexStartsWith(values) { }); } -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(''); @@ -2633,14 +2633,14 @@ 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) { @@ -2650,14 +2650,18 @@ function literalizeRegexPart(s: string) { 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 = {