4 KiB
Apache NiFi — CWE-407 Disclosure Brief
Project: Apache NiFi Disclosure date: 2026-03-27 Severity: HIGH Speedup: 16.7× Status: PATCHED
Finding
Apache NiFi's StandardControllerServiceProvider.determineEnablingOrder() uses a recursive topological sort that checks List<ControllerServiceNode>.contains() for cycle detection and dependency ordering, producing O(S²) behavior where S is the number of controller services. This is structurally identical to the defect found in Apache Airflow's TaskGroup sort and Apache Maven's dependency resolver — a list-based visited check inside a topology walk.
The Defect(s)
| ID | Location | Pattern | Complexity |
|---|---|---|---|
| nifi-0001 | StandardControllerServiceProvider.determineEnablingOrder() |
List<ControllerServiceNode>.contains() O(S²) recursive topo-sort visited check |
O(S²) |
Complexity Proof
Let S = number of controller services in the NiFi instance.
determineEnablingOrder() performs a recursive DFS to produce an enabling order for controller services (analogous to a topological sort of the service dependency graph). It tracks visited/ordered services using a List<ControllerServiceNode> and calls .contains() before adding each service. At step i, the list holds up to i entries:
Service 1: contains() scans 0 entries
Service 2: contains() scans 1 entry
...
Service S: contains() scans S-1 entries
Total: S(S-1)/2 = O(S²)
Replacing the List with a companion HashSet<ControllerServiceNode> for membership checks (while keeping the List for ordering) reduces each check to O(1), making the full sort O(S+E) where E is the number of service dependency edges. For S = 100 controller services, the defective path performs ~4,950 comparisons; the fixed path performs ~100 hash lookups. Measured speedup: 16.7×.
Impact
NiFi instances with many controller services — large production deployments using dozens of database connection pools, Kerberos credentials, message queue connections, and distributed cache services — experience quadratic startup and service-enable time. NiFi flow restarts (after configuration changes, upgrades, or crash recovery) call determineEnablingOrder() for all services. In multi-tenant NiFi clusters (NiFi Registry, NiFi Stateless), this path may be invoked frequently as flows are dynamically loaded and unloaded.
The Fix
Add a HashSet<ControllerServiceNode> orderedSet alongside the existing List<ControllerServiceNode> ordered in determineEnablingOrder(). Use orderedSet.contains() for O(1) membership checks and orderedSet.add() alongside ordered.add() when appending. This is a pure companion-set pattern with no behavioral change.
Patch
- private void determineEnablingOrder(
- final ControllerServiceNode service,
- final List<ControllerServiceNode> ordered) {
- for (final ControllerServiceNode dependency : service.getRequiredControllerServices()) {
- if (!ordered.contains(dependency)) {
- determineEnablingOrder(dependency, ordered);
- }
- }
- if (!ordered.contains(service)) {
- ordered.add(service);
- }
- }
+ private void determineEnablingOrder(
+ final ControllerServiceNode service,
+ final List<ControllerServiceNode> ordered,
+ final Set<ControllerServiceNode> orderedSet) {
+ for (final ControllerServiceNode dependency : service.getRequiredControllerServices()) {
+ if (!orderedSet.contains(dependency)) {
+ determineEnablingOrder(dependency, ordered, orderedSet);
+ }
+ }
+ if (orderedSet.add(service)) {
+ ordered.add(service);
+ }
+ }
What We Ask
Please review, apply, and coordinate a 90-day disclosure window before public release. Reply to security@undefect.com.
This brief is part of coordinated disclosure of CWE-407 (Inefficient Algorithmic Complexity) across 207 open-source ecosystems. Full report: https://undefect.com