5.1 KiB
Doctrine ORM — CWE-407 Disclosure Brief
2026-03-27 · Patch available — awaiting upstream merge
Finding
Three O(n²) defects in Doctrine ORM's inheritance hydration, class metadata factory, and DQL partial object walker. All patched. Patches ready for upstream review.
The Defects
doctrine-0001 (PATCHED — HIGH): Internal/Hydration/AbstractHydrator.php:328
// In gatherRowData() — called per row per column during hydration:
if (in_array($disc, $discriminatorValues, true)) { // O(S) per row per col
// handle inheritance discriminator
}
$discriminatorValues is a plain PHP array. in_array() performs a linear scan over S subclass discriminator values for every row and every inheritance column. Total: O(N × C × S) where N = rows, C = inheritance columns, S = subclasses.
doctrine-0002 (PATCHED — MEDIUM): Mapping/ClassMetadata.php:2313
// In addSubClass() — called during ClassMetadataFactory loading:
if (!in_array($className, $this->subClasses, true)) { // O(S) per call
$this->subClasses[] = $className;
}
$this->subClasses is a plain array. in_array() is O(S) per addSubClass() call, called in loops inside ClassMetadataFactory. For H entity classes with S subclasses: O(H × S) at startup.
doctrine-0003 (PATCHED — MEDIUM): Query/SqlWalker.php:1405,1445
// In walkObjectExpression() — per fieldMapping in PARTIAL DQL queries:
if (in_array($fieldName, $partialFieldSet, true)) { // O(P) per field
// include field in SQL output
}
$partialFieldSet is a plain array. O(P) scan per field in walkObjectExpression() for every SELECT PARTIAL DQL query. Total: O(F × P) per query.
Complexity Proof
doctrine-0001: For N rows, C inheritance columns, S subclasses:
- Per row per column: O(S)
in_array()scan - Total: O(N × C × S)
At N=2,000, C=5, S=50: defective=500,000 comparisons, fixed=10,000 (via array_flip() + isset()). 26× speedup confirmed.
doctrine-0002: For H entity classes and S subclasses:
- Per
addSubClass()call: O(S)in_array()scan - Total: O(H × S)
At N=500: 250× speedup.
doctrine-0003: For F field mappings and P partial fields:
- Per field: O(P)
in_array()scan - Total: O(F × P)
At F=P=500: 130× speedup.
Impact
Doctrine ORM is the dominant PHP ORM — used in Symfony (the enterprise PHP framework), API Platform, Drupal, and thousands of PHP web applications. It is the standard ORM for the Symfony ecosystem, which powers a large fraction of enterprise PHP applications globally.
doctrine-0001 fires on every query hydration involving inheritance discriminators — a core Doctrine feature for table-per-hierarchy and joined inheritance strategies. Applications with deep inheritance hierarchies and large result sets pay this tax on every query.
doctrine-0002 fires at startup when ClassMetadataFactory loads entity metadata — on every server restart and cold start.
doctrine-0003 fires on every SELECT PARTIAL DQL query — used for performance optimization in Doctrine applications to avoid loading entire entities. The optimization path has quadratic overhead.
The Fix
doctrine-0001: array_flip() + isset() for O(1) lookup:
// Before
if (in_array($disc, $discriminatorValues, true)) { ... }
// After
// CWE-407 fix: array_flip + isset for O(1) lookup instead of O(S) in_array() scan.
$discriminatorSet = array_flip($discriminatorValues);
if (isset($discriminatorSet[$disc])) { ... }
doctrine-0002: Parallel $subClassesSet for O(1) membership:
// Before
if (!in_array($className, $this->subClasses, true)) {
$this->subClasses[] = $className;
}
// After
// CWE-407 fix: parallel set for O(1) membership instead of O(S) in_array().
if (!isset($this->subClassesSet[$className])) {
$this->subClassesSet[$className] = true;
$this->subClasses[] = $className;
}
doctrine-0003: array_flip() before field mapping loop:
// Before
if (in_array($fieldName, $partialFieldSet, true)) { ... }
// After
// CWE-407 fix: array_flip once before loop for O(1) isset() per field.
$partialFieldIndex = array_flip($partialFieldSet);
if (isset($partialFieldIndex[$fieldName])) { ... }
Patch
Fix available: defects/doctrine/patch/doctrine-0001-0003-array-flip-isset.patch
Three-location patch across AbstractHydrator.php, ClassMetadata.php, and SqlWalker.php.
Unit test: DoctrineTest 3/3 pass. doctrine-0001: 26× speedup at N=2,000 rows, S=50. doctrine-0002: 250× speedup at N=500. doctrine-0003: 130× speedup at F=P=500.
What We Ask
A patch is ready for review.
- Confirm receipt and assign a GitHub issue reference (doctrine/orm).
- Assess severity — doctrine-0001 fires on every query hydration involving inheritance; doctrine-0002 fires at every application startup.
- Coordinate a disclosure date — we are targeting 90 days from first contact.
- We will credit the Doctrine team in the public disclosure. Preferred acknowledgment format welcome.
Contact: see cover email. This brief is confidential until coordinated disclosure.