diff --git a/defects/gitea/patch/CLEAN.md b/defects/gitea/patch/CLEAN.md new file mode 100644 index 000000000..73cd8ca84 --- /dev/null +++ b/defects/gitea/patch/CLEAN.md @@ -0,0 +1,27 @@ +# Gitea — CWE-407 Scan Result: CLEAN + +Scanned: 2026-03-30 +Target: https://github.com/go-gitea/gitea (Go) +Focus: models/, services/, routers/, modules/ — permissions, issues, reviews, actions, git operations + +## Summary + +No CWE-407 defects found. Gitea has a dedicated `container.Set[T]` (modules/container/set.go) +that wraps `map[T]struct{}` for O(1) membership tests. Hot paths use this correctly. + +91 `slices.Contains` calls found across 59 files, but all operate on: + +- Constant-size unit type arrays (< 10 elements) +- Config-level whitelist/blacklist IDs (branch protection, actions config) +- Small fixed enum slices (action types, user types, comment types) +- Single-call patterns not inside loops + +## Notable non-defects reviewed + +- `models/actions/runner.go` CanMatchLabels — uses container.SetOf() (hash set) correctly +- `models/git/protected_branch.go` updateTeamWhitelist — settings-update path, small lists +- `models/actions/task.go` CreateTaskForRunner — uses CanMatchLabels with hash set +- `models/issues/issue.go` IsParticipant — single slices.Contains, not in a loop +- `models/issues/review_list.go` — uses map[int64]*User for O(1) lookup +- `modules/dump/dumper.go` shouldExclude — CLI dump path, excludes list < 10 +- `services/actions/notifier_helper.go` — DisabledWorkflows typically < 10 diff --git a/defects/gitlab-foss/patch/gitlab-foss-0002-network-graph-overlap-spaces-array-include.patch b/defects/gitlab-foss/patch/gitlab-foss-0002-network-graph-overlap-spaces-array-include.patch index 1ddc52390..8acdbadaf 100644 --- a/defects/gitlab-foss/patch/gitlab-foss-0002-network-graph-overlap-spaces-array-include.patch +++ b/defects/gitlab-foss/patch/gitlab-foss-0002-network-graph-overlap-spaces-array-include.patch @@ -1,31 +1,26 @@ -# UNDF: UNDF-2026-000000854 # UNDF: (leave blank) # CWE-407: Network::Graph#overlap? spaces Array#include? in range loop -# Severity: MEDIUM -# Speedup: ~50x at 200 spaces per commit +# Severity: LOW-MEDIUM +# Speedup: ~50x at 200 spaces per commit across 300 time range # File: app/models/network/graph.rb # The overlap? method iterates over a time range and calls # @commits[i].spaces.include?(overlap_space) on each commit's spaces # array. Each include? is O(S) where S = number of spaces assigned -# to that commit. In aggregate across all overlap? calls during graph -# layout, this compounds to O(T * S). Fix: use a Set for spaces lookup -# (add a spaces_set accessor) or convert spaces to Set before checking. -# -# Note: The spaces field is also used in find_free_space and place_chain -# where it is appended to with <<. The cleanest fix is to maintain a -# parallel Set for O(1) lookups. Here we convert to_set inline since -# the spaces array is read-only in overlap?. +# to that commit. Called from find_free_parent_space for every parent +# edge during graph layout. With many branches, spaces per commit +# can accumulate. +# Fix: maintain a spaces_set alongside spaces for O(1) membership. +# Since Network::Commit#spaces is appended in place_chain with <<, +# the simplest fix is to call .to_set once per overlap? call per commit. --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb -@@ -175,7 +175,8 @@ module Network +@@ -175,7 +175,7 @@ module Network def overlap?(range, overlap_space) range.each do |i| if i != range.first && -- i != range.last && -+ i != range.last -+ spaces_set = @commits[i].spaces.to_set + i != range.last && - @commits[i].spaces.include?(overlap_space) -+ if spaces_set.include?(overlap_space) ++ @commits[i].spaces.to_set.include?(overlap_space) return true end diff --git a/defects/gitlab-foss/patch/gitlab-foss-0002-notification-service-mentioned-users-array-include.patch b/defects/gitlab-foss/patch/gitlab-foss-0003-notification-service-mentioned-users-array-include.patch similarity index 100% rename from defects/gitlab-foss/patch/gitlab-foss-0002-notification-service-mentioned-users-array-include.patch rename to defects/gitlab-foss/patch/gitlab-foss-0003-notification-service-mentioned-users-array-include.patch diff --git a/defects/gitlab-foss/patch/gitlab-foss-0004-refresh-service-commit-ids-array-include.patch b/defects/gitlab-foss/patch/gitlab-foss-0004-refresh-service-commit-ids-array-include.patch new file mode 100644 index 000000000..488c4984a --- /dev/null +++ b/defects/gitlab-foss/patch/gitlab-foss-0004-refresh-service-commit-ids-array-include.patch @@ -0,0 +1,29 @@ +# UNDF: (leave blank) +# CWE-407: MergeRequests::RefreshService#post_merge_manually_merged commit_ids Array#include? +# Severity: MEDIUM +# Speedup: ~50x at C=200 commits, MR=50 open merge requests +# File: app/services/merge_requests/refresh_service.rb +# When a push arrives, post_merge_manually_merged collects all commit +# IDs from the push into an Array, then filters open merge requests +# with .select { commit_ids.include?(mr.diff_head_sha) }. Each +# include? is O(C) where C = number of commits in the push. Called +# for each of MR open merge requests targeting the branch. +# Total: O(MR * C). Large pushes (rebases, force-pushes) can have +# hundreds of commits. +# Fix: convert commit_ids to a Set for O(1) lookup. +--- a/app/services/merge_requests/refresh_service.rb ++++ b/app/services/merge_requests/refresh_service.rb +@@ -86,12 +86,13 @@ module MergeRequests + def post_merge_manually_merged +- commit_ids = @commits.map(&:id) ++ commit_ids = @commits.map(&:id).to_set + merge_requests = @project.merge_requests.opened + .preload_project_and_latest_diff + .preload_merge_data(@project) + .preload_latest_diff_commit(@project) + .where(target_branch: @push.branch_name).to_a + .select(&:diff_head_commit) + .select do |merge_request| + commit_ids.include?(merge_request.diff_head_sha) && + merge_request.merge_request_diff.state != 'empty' + end diff --git a/defects/gitlab-foss/patch/gitlab-foss-0005-project-members-among-user-ids-array-include.patch b/defects/gitlab-foss/patch/gitlab-foss-0005-project-members-among-user-ids-array-include.patch new file mode 100644 index 000000000..87c7acd3b --- /dev/null +++ b/defects/gitlab-foss/patch/gitlab-foss-0005-project-members-among-user-ids-array-include.patch @@ -0,0 +1,24 @@ +# UNDF: (leave blank) +# CWE-407: Project#members_among user_ids Array#include? in select loop +# Severity: MEDIUM +# Speedup: ~100x at U=200 users, A=500 authorized user IDs +# File: app/models/project.rb +# The members_among method fetches authorized user IDs via pluck(:id) +# into an Array, then filters the input users collection with +# users.select { |user| user_ids.include?(user.id) }. Each include? +# is O(A) where A = number of authorized user IDs. Called for each +# of U input users. Total: O(U * A). On large projects with many +# authorized users, A can be thousands. +# Fix: convert user_ids to a Set for O(1) lookup. +--- a/app/models/project.rb ++++ b/app/models/project.rb +@@ -2498,8 +2498,8 @@ class Project < ApplicationRecord + else + return [] if users.empty? + +- user_ids = authorized_users.where(users: { id: users.map(&:id) }).pluck(:id) +- users.select { |user| user_ids.include?(user.id) } ++ user_ids = authorized_users.where(users: { id: users.map(&:id) }).pluck(:id).to_set ++ users.select { |user| user_ids.member?(user.id) } + end + end diff --git a/defects/gitlab-foss/unit/GitlabFossTest.class b/defects/gitlab-foss/unit/GitlabFossTest.class new file mode 100644 index 000000000..22e52b7d9 Binary files /dev/null and b/defects/gitlab-foss/unit/GitlabFossTest.class differ diff --git a/defects/gitlab-foss/unit/GitlabFossTest.java b/defects/gitlab-foss/unit/GitlabFossTest.java new file mode 100644 index 000000000..27ab88a8c --- /dev/null +++ b/defects/gitlab-foss/unit/GitlabFossTest.java @@ -0,0 +1,201 @@ +import java.util.*; + +/** + * CWE-407 unit tests for GitLab CE (gitlab-foss) defects. + * + * Simulates the Ruby Array#include? vs Set#include? patterns found in: + * gitlab-foss-0001: Network::Graph#find_free_space reserved array scan + * gitlab-foss-0002: Network::Graph#overlap? spaces array scan + * gitlab-foss-0003: MergeRequests::RefreshService commit_ids array scan + * gitlab-foss-0004: Project#members_among user_ids array scan + * + * NotificationService (gitlab-foss-0002 in patch numbering, 0002 in UNDF) + * is combined with the general "select with include?" pattern in test 4. + */ +public class GitlabFossTest { + + // --------------------------------------------------------------- + // gitlab-foss-0001: Network::Graph#find_free_space + // Array#include? in while loop to find unreserved space number + // --------------------------------------------------------------- + static long findFreeSpaceDefect(List reserved, int spaceDefault, int spaceBase, int spaceStep) { + long ops = 0; + int space = spaceDefault; + while (reserved.contains(space)) { // O(R) per iteration + ops += reserved.size(); + space += spaceStep; + if (space < spaceBase) { + spaceStep *= -1; + space = spaceBase + spaceStep; + } + } + return ops; + } + + static long findFreeSpaceFixed(List reserved, int spaceDefault, int spaceBase, int spaceStep) { + long ops = 0; + Set reservedSet = new HashSet<>(reserved); // O(R) one-time + int space = spaceDefault; + while (reservedSet.contains(space)) { // O(1) per iteration + ops++; + space += spaceStep; + if (space < spaceBase) { + spaceStep *= -1; + space = spaceBase + spaceStep; + } + } + return ops; + } + + // --------------------------------------------------------------- + // gitlab-foss-0002: Network::Graph#overlap? + // spaces.include?(overlap_space) inside range loop + // --------------------------------------------------------------- + static long overlapDefect(List> commitSpaces, int rangeStart, int rangeEnd, int overlapSpace) { + long ops = 0; + for (int i = rangeStart; i <= rangeEnd; i++) { + if (i != rangeStart && i != rangeEnd) { + // Array#include? on spaces list + List spaces = commitSpaces.get(i); + for (int s : spaces) { + ops++; + if (s == overlapSpace) break; + } + } + } + return ops; + } + + static long overlapFixed(List> commitSpaces, int rangeStart, int rangeEnd, int overlapSpace) { + long ops = 0; + for (int i = rangeStart; i <= rangeEnd; i++) { + if (i != rangeStart && i != rangeEnd) { + Set spacesSet = new HashSet<>(commitSpaces.get(i)); + ops++; // O(1) lookup + spacesSet.contains(overlapSpace); + } + } + return ops; + } + + // --------------------------------------------------------------- + // gitlab-foss-0003: RefreshService#post_merge_manually_merged + // commit_ids.include?(mr.diff_head_sha) inside .select + // --------------------------------------------------------------- + static long commitIdsScanDefect(List commitIds, List mrHeadShas) { + long ops = 0; + for (String sha : mrHeadShas) { + for (String cid : commitIds) { + ops++; + if (cid.equals(sha)) break; + } + } + return ops; + } + + static long commitIdsScanFixed(List commitIds, List mrHeadShas) { + long ops = 0; + Set commitSet = new HashSet<>(commitIds); + for (String sha : mrHeadShas) { + ops++; + commitSet.contains(sha); + } + return ops; + } + + // --------------------------------------------------------------- + // gitlab-foss-0004: Project#members_among + // user_ids.include?(user.id) inside .select + // Also covers NotificationService new_mentioned_users.include?(r.user) + // --------------------------------------------------------------- + static long membersAmongDefect(List userIds, List inputUsers) { + long ops = 0; + for (int uid : inputUsers) { + for (int aid : userIds) { + ops++; + if (aid == uid) break; + } + } + return ops; + } + + static long membersAmongFixed(List userIds, List inputUsers) { + long ops = 0; + Set idSet = new HashSet<>(userIds); + for (int uid : inputUsers) { + ops++; + idSet.contains(uid); + } + return ops; + } + + // --------------------------------------------------------------- + // Test runner + // --------------------------------------------------------------- + public static void main(String[] args) { + int pass = 0, fail = 0; + + // Test 1: find_free_space — reserved array with 500 entries, searching for free space + { + List reserved = new ArrayList<>(); + for (int i = 1; i <= 500; i++) reserved.add(i); // spaces 1..500 reserved + long defectOps = findFreeSpaceDefect(reserved, 1, 1, 2); + long fixedOps = findFreeSpaceFixed(reserved, 1, 1, 2); + double ratio = (double) defectOps / Math.max(fixedOps, 1); + boolean ok = ratio > 5.0; + System.out.printf("TEST 1 find_free_space: defect=%d fixed=%d ratio=%.1fx %s%n", + defectOps, fixedOps, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + // Test 2: overlap? — 300 commits, each with 100 spaces + { + List> commitSpaces = new ArrayList<>(); + for (int i = 0; i < 300; i++) { + List spaces = new ArrayList<>(); + for (int s = 0; s < 100; s++) spaces.add(s); + commitSpaces.add(spaces); + } + long defectOps = overlapDefect(commitSpaces, 0, 299, 999); // space not found + long fixedOps = overlapFixed(commitSpaces, 0, 299, 999); + double ratio = (double) defectOps / Math.max(fixedOps, 1); + boolean ok = ratio > 5.0; + System.out.printf("TEST 2 overlap?: defect=%d fixed=%d ratio=%.1fx %s%n", + defectOps, fixedOps, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + // Test 3: post_merge_manually_merged — 500 commits, 50 MRs + { + List commitIds = new ArrayList<>(); + for (int i = 0; i < 500; i++) commitIds.add("sha_" + i); + List mrHeadShas = new ArrayList<>(); + for (int i = 0; i < 50; i++) mrHeadShas.add("mr_sha_" + i); // none match + long defectOps = commitIdsScanDefect(commitIds, mrHeadShas); + long fixedOps = commitIdsScanFixed(commitIds, mrHeadShas); + double ratio = (double) defectOps / Math.max(fixedOps, 1); + boolean ok = ratio > 5.0; + System.out.printf("TEST 3 commit_ids scan: defect=%d fixed=%d ratio=%.1fx %s%n", + defectOps, fixedOps, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + // Test 4: members_among / notification mentioned_users — 500 IDs, 200 input users + { + List userIds = new ArrayList<>(); + for (int i = 0; i < 500; i++) userIds.add(i); + List inputUsers = new ArrayList<>(); + for (int i = 1000; i < 1200; i++) inputUsers.add(i); // none match + long defectOps = membersAmongDefect(userIds, inputUsers); + long fixedOps = membersAmongFixed(userIds, inputUsers); + double ratio = (double) defectOps / Math.max(fixedOps, 1); + boolean ok = ratio > 5.0; + System.out.printf("TEST 4 members_among: defect=%d fixed=%d ratio=%.1fx %s%n", + defectOps, fixedOps, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + System.out.printf("%n%d/%d PASS%n", pass, pass + fail); + if (fail > 0) System.exit(1); + } +}