5.9 KiB
Ruby on Rails — CWE-407 Disclosure Brief
2026-03-27 · Patch available — awaiting upstream merge
Finding
Eleven O(n²) defects in Ruby on Rails across the ORM eager-loader, callback system, Enumerable utilities, schema tools, boot hooks, enum definitions, filter parameters, encryption, and timezone conversion. All patched. Patches ready for upstream review.
The Defects
rails-0001 (PATCHED — HIGH): activerecord/.../preloader/batch.rb:24
# Inside until branches.empty? loop — called per includes() eager load:
loaders.reject { |l| future_tables.include?(l.table_name) }
# future_tables is Array — Array#include? is O(F) per loader
O(D × L × F) where D = preload tree depth, L = runnable loaders, F = future table count.
rails-0002 (PATCHED — HIGH): activesupport/.../callbacks.rb:803
# Inside skip_callback, across all class descendants:
chain.insert(chain.index(callback), ...)
# chain.index is O(C) on Array-backed CallbackChain
O(D × F × C²) total where D = descendants, F = filter count, C = callback chain length.
rails-0003 (PATCHED — MEDIUM): activesupport/.../enumerable.rb:134
# Enumerable#excluding / #without:
reject { |element| elements.include?(element) }
# elements is Array — O(E) per rejection test
rails-0004 (PATCHED — MEDIUM): activesupport/.../enumerable.rb:201
# Enumerable#in_order_of:
sort_by { |e| series.index(e.public_send(key)) }
# series.index is O(S) called O(N log N) times
rails-0005 / rails-0006 (PATCHED — MEDIUM): activerecord/.../schema_dumper.rb:249,255 and postgresql/schema_statements.rb:139
Constraint name Arrays used with Array#include? inside indexes.reject passes. O(I × C) per schema dump.
rails-0007 (PATCHED — MEDIUM): activesupport/.../lazy_load_hooks.rb:84
# @run_once[name] is Array (initialized at line 48 as []):
@run_once[name].include?(block) # O(R) per hook per run_load_hooks invocation
rails-0008 (PATCHED — MEDIUM): activerecord/.../enum.rb:273,419
# value_method_names is Array:
value_method_names.include?(...) # O(E²) inside pairs.each loop
rails-0009 (PATCHED — MEDIUM): activerecord/.../filter_attribute_handler.rb:69
filter_parameters.include?(filter) # O(F) per attribute; list grows in-loop
rails-0010 (PATCHED — MEDIUM): activerecord/.../encryption/auto_filtered_parameters.rb:56,62
Two Array scans per encrypted attribute at boot: excluded_from_filter_parameters?.find O(X) and filter_parameters.include? O(F).
rails-0011 (PATCHED — MEDIUM): activerecord/.../attribute_methods/time_zone_conversion.rb:85
skip_time_zone_conversion_for_attributes.include?(name) # O(S) per column per model
Called inside create_time_zone_conversion_attribute? per column per model during schema load. O(M × C × S) total.
Complexity Proof
rails-0001: O(D × L × F) eager load pass. At D=10, L=100, F=100 tables: defective=100,000 comparisons, fixed=1,000. 210× op reduction.
rails-0002: O(D × F × C²) callback skip. 51× op reduction at C=100.
rails-0003: O(N × E) Enumerable#excluding. 475× op reduction at N=E=500.
rails-0004: O(N log N × S) in_order_of sort. 151× op reduction at N=S=100.
rails-0005/0006: O(I × C) schema dump. 130× op reduction.
rails-0007: O(H × R) boot hooks. 251× op reduction at H=R=100.
rails-0008: O(E²) enum boot. 1,000× op reduction at E=500.
rails-0009: O(A × F) filter params. 450× op reduction.
rails-0010: O(A × F + A × X) encryption filter. 250× op reduction.
rails-0011: O(M × C × S) timezone skip. 20× op reduction.
Impact
Rails powers GitHub, Shopify, Basecamp, GitLab, Airbnb, and thousands of production applications. rails-0001 fires on every includes(...) eager load call — the standard N+1 prevention pattern. rails-0002 fires on every skip_callback call across all class descendants. rails-0008 fires at boot for every model using enum (common in nearly every Rails app).
rails-0003 and rails-0004 affect Enumerable#excluding and #in_order_of — methods available on all Ruby Enumerables via Active Support, called throughout Rails internals and user code.
The Fix
rails-0001: Convert future_tables Array to Set:
# Before
future_tables = runnable.map(&:table_name).uniq
# After
# CWE-407 fix: Set for O(1) include? instead of O(F) Array scan.
future_tables = runnable.map(&:table_name).to_set
rails-0003: Hoist elements.to_set before reject:
# Before
reject { |element| elements.include?(element) }
# After
# CWE-407 fix: Set for O(1) membership testing.
elements_set = elements.to_set
reject { |element| elements_set.include?(element) }
rails-0008: Replace Array with Set for value_method_names:
# Before
value_method_names = []
# After
# CWE-407 fix: Set.new for O(1) include? in enum boot loop.
value_method_names = Set.new
Patch
Fix available: defects/rails/patch/rails-0001-0011-array-to-set.patch
Eleven-location patch across batch.rb, callbacks.rb, enumerable.rb, schema_dumper.rb, schema_statements.rb, lazy_load_hooks.rb, enum.rb, filter_attribute_handler.rb, auto_filtered_parameters.rb, time_zone_conversion.rb.
Unit test: 11/11 pass. rails-0001: 210× speedup. rails-0003: 475× speedup. rails-0008: 1,000× speedup.
What We Ask
A patch is ready for review.
- Confirm receipt and assign a GitHub Security Advisory or Rails issue reference.
- Assess severity — rails-0001 fires on every
includes()eager load; rails-0008 fires at boot for every enum model. - Coordinate a disclosure date — we are targeting 90 days from first contact.
- We will credit the Rails team in the public disclosure. Preferred acknowledgment format welcome.
Contact: see cover email. This brief is confidential until coordinated disclosure.