Security advisory GHSA-cfw5-68c4-ffqp: MikroORM has SQL injection via runtime-controlled identifiers and JSON-path keys
Package: @mikro-orm/sql (npm), vulnerable version: 7.0.13, fixed in: 7.0.14
CWE: CWE-89
Files changed by the fix:
packages/core/src/platforms/Platform.ts
packages/mssql/src/MsSqlPlatform.ts
packages/postgresql/src/PostgreSqlPlatform.ts

Fix diff:
diff --git a/packages/core/src/platforms/Platform.ts b/packages/core/src/platforms/Platform.ts
index e34a03d52859..c3b5c3e2f444 100644
--- a/packages/core/src/platforms/Platform.ts
+++ b/packages/core/src/platforms/Platform.ts
@@ -477,7 +477,8 @@ export abstract class Platform {
   }
 
   quoteIdentifier(id: string | { toString: () => string }, quote = '`'): string {
-    return `${quote}${id.toString().replace('.', `${quote}.${quote}`)}${quote}`;
+    const escaped = id.toString().replaceAll(quote, quote + quote);
+    return `${quote}${escaped.replace('.', `${quote}.${quote}`)}${quote}`;
   }
 
   quoteValue(value: any): string {
diff --git a/packages/mssql/src/MsSqlPlatform.ts b/packages/mssql/src/MsSqlPlatform.ts
index 7ee06c8bfb56..1b84f96aaba8 100644
--- a/packages/mssql/src/MsSqlPlatform.ts
+++ b/packages/mssql/src/MsSqlPlatform.ts
@@ -196,7 +196,7 @@ export class MsSqlPlatform extends AbstractSqlPlatform {
   override getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string {
     const [a, ...b] = path;
     /* istanbul ignore next */
-    const root = this.quoteIdentifier(aliased ? `${ALIAS_REPLACEMENT}.${a}` : a);
+    const root = aliased ? `[${ALIAS_REPLACEMENT}].${this.quoteIdentifier(a)}` : this.quoteIdentifier(a);
     const types = {
       boolean: 'bit',
     } as Dictionary;
@@ -229,7 +229,7 @@ export class MsSqlPlatform extends AbstractSqlPlatform {
   }
 
   override quoteIdentifier(id: string | { toString: () => string }): string {
-    return `[${id.toString().replace('.', `].[`)}]`;
+    return `[${id.toString().replaceAll(']', ']]').replace('.', `].[`)}]`;
   }
 
   override escape(value: any): string {
diff --git a/packages/postgresql/src/PostgreSqlPlatform.ts b/packages/postgresql/src/PostgreSqlPlatform.ts
index af066d3a5f2d..307435c7420b 100644
--- a/packages/postgresql/src/PostgreSqlPlatform.ts
+++ b/packages/postgresql/src/PostgreSqlPlatform.ts
@@ -320,7 +320,8 @@ export class PostgreSqlPlatform extends AbstractSqlPlatform {
   }
 
   override quoteIdentifier(id: string | { toString: () => string }, quote = '"'): string {
-    return `${quote}${id.toString().replace('.', `${quote}.${quote}`)}${quote}`;
+    const escaped = id.toString().replaceAll(quote, quote + quote);
+    return `${quote}${escaped.replace('.', `${quote}.${quote}`)}${quote}`;
   }
 
   override escape(value: any): string {
diff --git a/tests/platforms/identifier-quoting.test.ts b/tests/platforms/identifier-quoting.test.ts
new file mode 100644
index 000000000000..256ab6be46ab
--- /dev/null
+++ b/tests/platforms/identifier-quoting.test.ts
@@ -0,0 +1,34 @@
+import { MySqlPlatform } from '@mikro-orm/mysql';
+import { MariaDbPlatform } from '@mikro-orm/mariadb';
+import { SqlitePlatform } from '@mikro-orm/sqlite';
+import { PostgreSqlPlatform } from '@mikro-orm/postgresql';
+import { MsSqlPlatform } from '@mikro-orm/mssql';
+
+describe('quoteIdentifier escapes embedded quote characters', () => {
+  test.each([
+    ['mysql', new MySqlPlatform()],
+    ['mariadb', new MariaDbPlatform()],
+    ['sqlite', new SqlitePlatform()],
+  ])('backtick dialect [%s]', (_, platform) => {
+    expect(platform.quoteIdentifier('table')).toBe('`table`');
+    expect(platform.quoteIdentifier('schema.table')).toBe('`schema`.`table`');
+    expect(platform.quoteIdentifier('a`b')).toBe('`a``b`');
+    expect(platform.quoteIdentifier('a`.b')).toBe('`a```.`b`');
+  });
+
+  test('double-quote dialect [postgres]', () => {
+    const platform = new PostgreSqlPlatform();
+    expect(platform.quoteIdentifier('table')).toBe('"table"');
+    expect(platform.quoteIdentifier('schema.table')).toBe('"schema"."table"');
+    expect(platform.quoteIdentifier('a"b')).toBe('"a""b"');
+    expect(platform.quoteIdentifier('a".b')).toBe('"a"""."b"');
+  });
+
+  test('bracket dialect [mssql]', () => {
+    const platform = new MsSqlPlatform();
+    expect(platform.quoteIdentifier('table')).toBe('[table]');
+    expect(platform.quoteIdentifier('schema.table')).toBe('[schema].[table]');
+    expect(platform.quoteIdentifier('a]b')).toBe('[a]]b]');
+    expect(platform.quoteIdentifier('a].b')).toBe('[a]]].[b]');
+  });
+});