java-topology/whitepaper/outreach/knex.md
russell@unturf.com cd28454d2d
wave6: knex-0001 flagship + 38-target docgen/webfw/migration scan survey
knex-0001: Migrator#rollback({all:true}) and Migrator#down() filter
allMigrations against completed via .map(name).includes() inside the
filter callback. Per-iter array allocation + linear scan = O(A*C)
real, O(A*C^2) amortized including GC. Fix: hoist Set<name> once,
Set#has = O(1). Bench: 355x at A=C=2000 migrations.

wave6-docgen-webfw-tui-survey.md: 38-target scan covering doc gens
(Sphinx, JSDoc, TypeDoc, Doxygen, MkDocs, Hugo, Jekyll, Gatsby,
Eleventy, Astro), web frameworks (Fastify, Express, Koa, hapi,
SvelteKit, Nuxt, Remix), TUI/CLI (Cobra, Click, Commander.js, Yargs,
Bubble Tea, Ratatui), migrations (Flyway, Goose, dbmate, Knex,
Sqitch, Atlas), search engines (Tantivy, MeiliSearch, Typesense),
API gateways (Kong, APISIX), MQTT/queue brokers (Mosquitto, EMQX,
VerneMQ, ZeroMQ).

Clean-scan honor roll +3: Bubble Tea, dbmate, libzmq.
2026-04-25 10:10:12 -04:00

2.3 KiB
Raw Blame History

Knex.js — CWE-407 Disclosure Brief

Project: Knex.js (knex/knex) Disclosure date: 2026-04-25 Severity: MEDIUM-HIGH Speedup: 355× measured at A=C=2000 migrations Status: patch-ready, 1 patch + bench


Summary

Knex.js is the dominant SQL migration library in the Node ecosystem. Its Migrator#rollback({all:true}) and Migrator#down() methods filter the full migration list against the completed-migrations list using a per-iteration .map(name).includes(...) chain. For A all-migrations and C completed-migrations, the per-call cost is O(A×C) compute plus O(A×C) allocation — effectively O(A×C²) when accounting for GC pressure on the rebuilt names array.

Mature databases (long-lived Rails-style projects ported to Node, monorepos with many service schemas) carry hundreds of migrations. Every knex migrate:rollback --all and knex migrate:down run pays this. Bench shows 355× speedup at A=C=2000 once the lookup is hoisted into a Set.

The Defects

knex-0001 (MOAD-0001 — MEDIUM-HIGH): lib/migrations/migrate/Migrator.js:188-194, 217-222

// rollback({all: true}) — line 188
allMigrations.filter((migration) => {
  return completedMigrations
    .map((migration) => migration.name)        // O(C) array allocation per filter step
    .includes(this.config.migrationSource.getMigrationName(migration));  // O(C) scan
}).reverse();

// down() — line 217 — same pattern
const completedMigrations = all.filter((migration) => {
  return completed
    .map((migration) => migration.name)
    .includes(this.config.migrationSource.getMigrationName(migration));
});

Fix: Hoist a Set<name> outside the filter; Set#has is O(1).

Benchmark (A all × C completed) defective fixed speedup
200×200 4.70ms 0.08ms 56.2×
500×500 27.91ms 0.19ms 144.7×
1000×1000 75.14ms 0.37ms 202.5×
2000×2000 290.10ms 0.82ms 355.5×

Scanner Evidence

unmoad flags both call sites at HIGH severity via the array-includes-in-loop rule. Trigger captures the .map().includes() rebuilt-array pattern.

Patches

  • knex-0001-migrator-completed-name-set.patch