5 KiB
Exposed ORM (Kotlin) — CWE-407 Disclosure Brief
2026-03-27 · Patch available — awaiting upstream merge
Finding
Three O(n²) defects in JetBrains Exposed ORM across schema migration, SQL keyword checking, and table clone operations. All patched. Patches ready for upstream review. Exposed is the Kotlin SQL framework used in Ktor backends and Android applications.
The Defects
exposed-0001 (PATCHED — HIGH): SchemaUtilityApi.kt:80
// In mapMissingColumnStatements() — called during schema migration:
val existingCol = existingColumns.find { it.name.lowercase() == col.name.lowercase() }
// existingColumns is List<Column> — find{} is O(M) per column
// Also:
if (!missingTableColumns.contains(col)) { ... }
// missingTableColumns is List — contains() is O(M) per index-column
O(M²) over M columns during schema migration — called on every SchemaUtils.createMissingTablesAndColumns().
exposed-0002 (PATCHED — MEDIUM): IdentifierManagerApi.kt:72
// On every SQL generation cache miss — per identifier:
keywords.any { equals(it, true) }
// keywords is List<String> of ~504 SQL reserved words — O(K) per call
O(K) scan over ~504 SQL keywords per identifier on every SQL generation cache miss. Every column name, table name, and alias triggers this on first use.
exposed-0003 (PATCHED — MEDIUM): Table.kt:1686
// In T.clone() — per property:
consParams.map(KParameter::name) // O(C) fresh List allocation per property filter pass
.contains(it.name) // O(C) scan
Allocates a fresh List<String?> from consParams on every property filter iteration in clone(). O(P × C) total.
Complexity Proof
exposed-0001: For M columns in a table:
- Per column: O(M)
find{}scan through existing columns - Total: O(M²)
At M=500: defective=125,000 comparisons, fixed=500 (via associateBy map). 118× op reduction.
exposed-0002: For K=504 keywords per identifier check:
- Each cache miss: O(K) linear scan
- Total per query: O(identifiers × K)
At K=504 keywords: defective=504 comparisons per miss, fixed=1 (HashSet lookup). 144× op reduction.
exposed-0003: For P properties and C constructor parameters:
- Per property: O(C) allocation + O(C) scan
- Total: O(P × C)
At P=20 properties, C=15 parameters: 6× op reduction (lower ratio because P and C are bounded in typical use).
Impact
JetBrains Exposed is the Kotlin ORM for Ktor and Android backends — used across JetBrains tooling, Kotlin-first microservices, and Android Room alternatives. exposed-0001 fires on every database schema migration, which runs at startup in development and in CI/CD pipelines. For tables with many columns (audit tables, wide entity tables), startup time scales quadratically with column count.
exposed-0002 fires on every SQL generation cache miss — i.e., on every unique query structure the first time it is compiled. In dynamic query builders and applications with many distinct query shapes, this is a hot path.
The Fix
exposed-0001: Build an associateBy map and a HashSet before loops:
// Before
val existingCol = existingColumns.find { it.name.lowercase() == col.name.lowercase() }
// After
// CWE-407 fix: associateBy map for O(1) lookup instead of O(M) find{} per column.
val existingColMap = existingColumns.associateBy { it.name.lowercase() }
val existingCol = existingColMap[col.name.lowercase()]
exposed-0002: Lazy lowercase HashSet for keywords:
// Before
keywords.any { equals(it, true) }
// After
// CWE-407 fix: lazy HashSet for O(1) contains() instead of O(K) linear scan.
private val keywordsSet: Set<String> by lazy { keywords.map { it.lowercase() }.toHashSet() }
keywordsSet.contains(identifier.lowercase())
exposed-0003: Hoist HashSet before property loop:
// Before
consParams.map(KParameter::name).contains(it.name)
// After
// CWE-407 fix: hoist HashSet before loop instead of allocating fresh List per iteration.
val consParamNames = consParams.map(KParameter::name).toHashSet()
consParamNames.contains(it.name)
Patch
Fix available: defects/exposed/patch/exposed-0001-0003-associateby-hashset.patch
Three-location patch across SchemaUtilityApi.kt, IdentifierManagerApi.kt, and Table.kt.
Unit test: ExposedTest 3/3 pass. exposed-0001: 118× speedup at N=500 cols. exposed-0002: 144× speedup at K=504. exposed-0003: 6× speedup.
What We Ask
A patch is ready for review.
- Confirm receipt and assign a YouTrack or GitHub issue reference (JetBrains/Exposed).
- Assess severity — exposed-0001 fires on every schema migration at startup; exposed-0002 fires on every unique query structure compilation.
- Coordinate a disclosure date — we are targeting 90 days from first contact.
- We will credit the Exposed/JetBrains team in the public disclosure. Preferred acknowledgment format welcome.
Contact: see cover email. This brief is confidential until coordinated disclosure.