java-topology/whitepaper/outreach/redmine.md
russell@unturf.com 283a490d6d feat: add 5 outreach docs (15 defects) for batch 4
distlib (3, Python), redmine (3, Ruby), grape (3, Ruby),
solc (3, Solidity/C++), grpc-java (3, Java).
2026-04-13 16:55:04 -04:00

5.1 KiB
Raw Blame History

Redmine — CWE-407 Disclosure Brief

2026-04-13 · Patch available — awaiting upstream merge

Finding

Three O(n²) defects in Redmine's issue tracking and user authorization. All patched. Two defects fire during issue relationship traversal (blocking and rescheduling checks); one fires during user role computation. All three use Array membership tests where Set lookups belong.

The Defects

redmine-0001 (PATCHED — HIGH): app/models/issue.rb:1332

# In Issue#blocks?(other) — fires on issue dependency checks:
all = [self]
last = [self]
while last.any?
  current = last.map { |i|
    i.relations_from.where(relation_type: IssueRelation::TYPE_BLOCKS).map(&:issue_to)
  }.flatten.uniq
  current -= last
  current -= all    # Array subtraction: O(|current| × |all|) per BFS level
  return true if current.include?(other)
  last = current
  all += last
end

all grows as a plain Array. The -= operator performs O(|current| x |all|) comparisons at each BFS level. In deep issue dependency chains, all accumulates every visited node, making each level progressively more expensive.

redmine-0002 (PATCHED — HIGH): app/models/issue.rb:1352

# In Issue#would_reschedule?(other) — fires on date change propagation:
all = [self]
last = [self]
while last.any?
  current = last.map { |i|
    i.relations_from.where(relation_type: IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
    i.leaves.to_a +
    i.ancestors.map { |a| ... }
  }.flatten.uniq
  current -= last
  current -= all    # Same O(|current| × |all|) Array scan
  ...
  all += last
end

Identical pattern to redmine-0001. Fires whenever Redmine checks whether changing start/due dates of one issue would cascade rescheduling to another. Broader traversal (precedes + leaves + ancestors) means larger BFS frontier per level.

redmine-0003 (PATCHED — MEDIUM): app/models/user.rb:700

# In User#projects_by_role — fires on every authorization check:
members.each do |user_id, role_id, project_id|
  next if user_id != id && project_ids.include?(project_id)  # O(P) Array#include?
  hash[role_id] ||= []
  hash[role_id] << project_id
end

project_ids is a plain Array. project_ids.include?(project_id) performs a linear scan for every member row returned from the database. With M member rows and P projects: O(M x P) total comparisons.

Complexity Proof

redmine-0001 / redmine-0002: At n=500 issues in a dependency chain, branching factor 3:

  • Defective: each BFS level subtracts against a growing all array. Over ~10 levels: sum of |current| x |all| at each level approaches ~125,000 comparisons
  • Fixed: Set membership at O(1) per check = ~1,500 operations
  • ~80x op reduction.

redmine-0003: At M=10,000 member rows, P=200 projects:

  • Defective: 10,000 × 200 = 2,000,000 comparisons
  • Fixed: 10,000 × O(1) Set lookups = 10,000 operations
  • 200x op reduction.

Impact

Redmine serves millions of users across enterprise and open-source project management. Issue dependency checks (blocks?, would_reschedule?) fire on every issue update that involves blocking or precedence relationships. In projects with hundreds of interlinked issues, these BFS traversals hit quadratic scaling on every save.

redmine-0003 fires on every authorization check for users with many project memberships. Enterprise Redmine instances with thousands of projects and complex role assignments hit this path on every page load that checks permissions.

The Fix

redmine-0001: Replace Array all with Set:

# Before
all = [self]
current -= all
all += last

# After
# CWE-407 fix: Set for O(1) membership instead of Array O(N).
all = Set.new([self])
current.reject! { |c| all.include?(c) }  # O(1) per element with Set
all.merge(last)

redmine-0002: Same pattern as redmine-0001:

# Before
all = [self]
current -= all
all += last

# After
all = Set.new([self])
current.reject! { |c| all.include?(c) }
all.merge(last)

redmine-0003: Convert project_ids to Set before the loop:

# Before
next if user_id != id && project_ids.include?(project_id)

# After
# CWE-407 fix: O(1) lookup instead of O(P) Array#include?.
project_ids_set = project_ids.to_set
next if user_id != id && project_ids_set.include?(project_id)

Patch

Fix available: defects/redmine/patch/redmine-0001-issue-blocks-bfs-array.patch, defects/redmine/patch/redmine-0002-issue-would_reschedule-bfs-array.patch, defects/redmine/patch/redmine-0003-user-project_ids-include.patch

Three patches across issue.rb and user.rb.

redmine-0001/0002: ~80x speedup at n=500 issues. redmine-0003: 200x speedup at M=10,000 members, P=200 projects.

What We Ask

A patch is ready for review.

  1. Confirm receipt and assign a tracker reference (redmine/redmine).
  2. Assess severity — redmine-0001/0002 fire on every issue update with blocking/precedence relations; redmine-0003 fires on every authorization check.
  3. Coordinate a disclosure date — we are targeting 90 days from first contact.
  4. We will credit the Redmine team in the public disclosure. Preferred acknowledgment format welcome.

Contact: see cover email. This brief is confidential until coordinated disclosure.