diff --git a/UNDF-REGISTRY.json b/UNDF-REGISTRY.json index 4f8d0b68f..5e09678be 100644 --- a/UNDF-REGISTRY.json +++ b/UNDF-REGISTRY.json @@ -873,5 +873,8 @@ "suitecrm-0002": "UNDF-2026-000000872", "suitecrm-0003": "UNDF-2026-000000873", "vllm-0001": "UNDF-2026-000000874", - "suricata-0001-0001": "UNDF-2026-000000875" + "suricata-0001-0001": "UNDF-2026-000000875", + "redmine-0001": "UNDF-2026-000000876", + "redmine-0002": "UNDF-2026-000000877", + "redmine-0003": "UNDF-2026-000000878" } diff --git a/defects/redmine/patch/redmine-0001-issue-blocks-bfs-array.patch b/defects/redmine/patch/redmine-0001-issue-blocks-bfs-array.patch index de1ffb772..c3b99cb2f 100644 --- a/defects/redmine/patch/redmine-0001-issue-blocks-bfs-array.patch +++ b/defects/redmine/patch/redmine-0001-issue-blocks-bfs-array.patch @@ -1,3 +1,4 @@ +# UNDF: UNDF-2026-000000876 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1332,15 +1332,16 @@ diff --git a/defects/redmine/patch/redmine-0002-issue-would_reschedule-bfs-array.patch b/defects/redmine/patch/redmine-0002-issue-would_reschedule-bfs-array.patch index a93d8b05e..93c4866e1 100644 --- a/defects/redmine/patch/redmine-0002-issue-would_reschedule-bfs-array.patch +++ b/defects/redmine/patch/redmine-0002-issue-would_reschedule-bfs-array.patch @@ -1,3 +1,4 @@ +# UNDF: UNDF-2026-000000877 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1352,7 +1352,7 @@ diff --git a/defects/redmine/patch/redmine-0003-user-project_ids-include.patch b/defects/redmine/patch/redmine-0003-user-project_ids-include.patch new file mode 100644 index 000000000..ed7ab8cd2 --- /dev/null +++ b/defects/redmine/patch/redmine-0003-user-project_ids-include.patch @@ -0,0 +1,18 @@ +# UNDF: UNDF-2026-000000878 +--- a/app/models/user.rb ++++ b/app/models/user.rb +@@ -700,9 +700,10 @@ + members = Member.joins(:project, :member_roles). + where("#{Project.table_name}.status <> 9"). + where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Member.table_name}.user_id = ?)", self.id, true, group_id). + pluck(:user_id, :role_id, :project_id) + ++ project_ids_set = project_ids.to_set # O(1) lookup instead of O(P) Array#include? + hash = {} + members.each do |user_id, role_id, project_id| + # Ignore the roles of the builtin group if the user is a member of the project +- next if user_id != id && project_ids.include?(project_id) ++ next if user_id != id && project_ids_set.include?(project_id) + + hash[role_id] ||= [] + hash[role_id] << project_id diff --git a/defects/redmine/test/RedmineCWE407Test.class b/defects/redmine/test/RedmineCWE407Test.class new file mode 100644 index 000000000..fb97c55ad Binary files /dev/null and b/defects/redmine/test/RedmineCWE407Test.class differ diff --git a/defects/redmine/test/RedmineCWE407Test.java b/defects/redmine/test/RedmineCWE407Test.java new file mode 100644 index 000000000..82470b196 --- /dev/null +++ b/defects/redmine/test/RedmineCWE407Test.java @@ -0,0 +1,192 @@ +import java.util.*; + +/** + * CWE-407 unit tests for Redmine defects. + * + * redmine-0001: issue.rb blocks?() BFS uses Array for 'all' visited set — O(V^2) subtraction + * redmine-0002: issue.rb would_reschedule?() same BFS Array pattern — O(V^2) + * redmine-0003: user.rb project_ids_by_role project_ids.include? in members.each — O(M*P) + */ +public class RedmineCWE407Test { + + // ---- redmine-0001: blocks? BFS with Array visited set ---- + + /** Unpatched: Array 'all' grows, Array subtraction O(C*A) per BFS layer */ + static boolean blocksBfsUnpatched(int startId, int targetId, Map> graph) { + List all = new ArrayList<>(); + all.add(startId); + List last = new ArrayList<>(); + last.add(startId); + + while (!last.isEmpty()) { + Set currentSet = new LinkedHashSet<>(); + for (int node : last) { + List neighbors = graph.getOrDefault(node, Collections.emptyList()); + currentSet.addAll(neighbors); + } + List current = new ArrayList<>(currentSet); + + // Array subtraction: current -= last, current -= all (both O(N) per element) + current.removeAll(last); + Iterator it = current.iterator(); + while (it.hasNext()) { + if (all.contains(it.next())) { // O(A) per element + it.remove(); + } + } + + if (current.contains(targetId)) return true; + + last = current; + all.addAll(last); + } + return false; + } + + /** Patched: Set 'all' for O(1) membership — O(V+E) total */ + static boolean blocksBfsPatched(int startId, int targetId, Map> graph) { + Set all = new HashSet<>(); + all.add(startId); + List last = new ArrayList<>(); + last.add(startId); + + while (!last.isEmpty()) { + Set currentSet = new LinkedHashSet<>(); + for (int node : last) { + List neighbors = graph.getOrDefault(node, Collections.emptyList()); + currentSet.addAll(neighbors); + } + List current = new ArrayList<>(currentSet); + + // Set-based removal: O(1) per element + current.removeIf(all::contains); + + if (current.contains(targetId)) return true; + + last = current; + all.addAll(last); + } + return false; + } + + // ---- redmine-0003: user project_ids membership ---- + + /** Unpatched: project_ids.include? in members.each — O(M*P) */ + static Map> projectIdsByRoleUnpatched( + int userId, List members, List projectIds) { + Map> hash = new HashMap<>(); + for (int[] member : members) { + int memberUserId = member[0]; + int roleId = member[1]; + int projectId = member[2]; + + // Skip builtin group roles if user is a member of this project + if (memberUserId != userId && projectIds.contains(projectId)) { // O(P) per member + continue; + } + hash.computeIfAbsent(roleId, k -> new ArrayList<>()).add(projectId); + } + return hash; + } + + /** Patched: project_ids.to_set for O(1) lookup — O(M+P) */ + static Map> projectIdsByRolePatched( + int userId, List members, List projectIds) { + Set projectIdsSet = new HashSet<>(projectIds); // O(P) once + Map> hash = new HashMap<>(); + for (int[] member : members) { + int memberUserId = member[0]; + int roleId = member[1]; + int projectId = member[2]; + + if (memberUserId != userId && projectIdsSet.contains(projectId)) { // O(1) per member + continue; + } + hash.computeIfAbsent(roleId, k -> new ArrayList<>()).add(projectId); + } + return hash; + } + + // ---- Test harness ---- + + static void testRedmine0001and0002() { + System.out.println("=== redmine-0001/0002: issue BFS blocks?/would_reschedule? ==="); + // Build a chain graph: 0 -> 1 -> 2 -> ... -> V-1 + int V = 20000; + Map> graph = new HashMap<>(); + for (int i = 0; i < V - 1; i++) { + graph.put(i, List.of(i + 1)); + } + int targetId = V - 1; + + // Warmup + blocksBfsPatched(0, targetId, graph); + + long t0 = System.nanoTime(); + boolean r1 = blocksBfsUnpatched(0, targetId, graph); + long unpatched = System.nanoTime() - t0; + + t0 = System.nanoTime(); + boolean r2 = blocksBfsPatched(0, targetId, graph); + long patched = System.nanoTime() - t0; + + assert r1 == r2 : "Result mismatch"; + assert r1 == true : "Expected true"; + + double ratio = (double) unpatched / patched; + System.out.printf(" V=%d unpatched=%dms patched=%dms ratio=%.1fx%n", + V, unpatched / 1_000_000, patched / 1_000_000, ratio); + assert ratio > 3.0 : "Expected >3x ratio, got " + ratio; + System.out.println(" PASS (covers both blocks? and would_reschedule?)"); + } + + static void testRedmine0003() { + System.out.println("=== redmine-0003: user project_ids_by_role membership ==="); + int M = 50000; // membership rows + int P = 5000; // user's projects + int userId = 1; + + List projectIds = new ArrayList<>(P); + for (int i = 0; i < P; i++) { + projectIds.add(i); + } + + // Generate member rows: mix of user's own and builtin group entries + List members = new ArrayList<>(M); + Random rng = new Random(42); + for (int i = 0; i < M; i++) { + int uid = rng.nextBoolean() ? userId : 999; // 50% user, 50% builtin group + int roleId = rng.nextInt(10); + int projectId = rng.nextInt(P * 2); // some in user's projects, some not + members.add(new int[]{uid, roleId, projectId}); + } + + // Warmup + projectIdsByRolePatched(userId, members, projectIds); + + long t0 = System.nanoTime(); + Map> r1 = projectIdsByRoleUnpatched(userId, members, projectIds); + long unpatched = System.nanoTime() - t0; + + t0 = System.nanoTime(); + Map> r2 = projectIdsByRolePatched(userId, members, projectIds); + long patched = System.nanoTime() - t0; + + // Verify same results + int totalR1 = r1.values().stream().mapToInt(List::size).sum(); + int totalR2 = r2.values().stream().mapToInt(List::size).sum(); + assert totalR1 == totalR2 : "Result mismatch: " + totalR1 + " vs " + totalR2; + + double ratio = (double) unpatched / patched; + System.out.printf(" M=%d P=%d unpatched=%dms patched=%dms ratio=%.1fx%n", + M, P, unpatched / 1_000_000, patched / 1_000_000, ratio); + assert ratio > 3.0 : "Expected >3x ratio, got " + ratio; + System.out.println(" PASS"); + } + + public static void main(String[] args) { + testRedmine0001and0002(); + testRedmine0003(); + System.out.println("\nAll 3 Redmine CWE-407 tests PASSED."); + } +}