Decidim has race condition in Endorsements
Description
Decidim is a participatory democracy framework. Starting in version 0.10.0 and prior to versions 0.26.9, 0.27.5, and 0.28.0, a race condition in the endorsement of resources (for instance, a proposal) allows a user to make more than once endorsement. To exploit this vulnerability, the request to set an endorsement must be sent several times in parallel. Versions 0.26.9, 0.27.5, and 0.28.0 contain a patch for this issue. As a workaround, disable the Endorsement feature in the components.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
decidimRubyGems | >= 0.10.0, < 0.26.9 | 0.26.9 |
decidimRubyGems | >= 0.27.0, < 0.27.5 | 0.27.5 |
Affected products
1Patches
25c5ee7a50d75Backport 'Fix duplicated endorsements' to v0.26 (#11973)
7 files changed · +83 −5
CHANGELOG.md+12 −0 modified@@ -2,6 +2,18 @@ ## [Unreleased](https://github.com/decidim/decidim/tree/HEAD) +### Upgrade notes + +#### Deduplicating endorsements + +We have identified a case when the same user can endorse the same resource multiple times. This is a bug that we have fixed in this release, but we need to clean up the existing duplicated endorsements. We have added a new task that helps you clean the duplicated endorsements. + +```bash +bundle exec rails decidim:upgrade:fix_duplicate_endorsements +``` + +You can see more details about this change on PR [\#11853](https://github.com/decidim/decidim/pull/11853) + ### Added Nothing.
decidim-core/app/commands/decidim/endorse_resource.rb+2 −0 modified@@ -31,6 +31,8 @@ def call else broadcast(:invalid) end + rescue ActiveRecord::RecordNotUnique + broadcast(:invalid) end private
decidim-core/app/commands/decidim/unendorse_resource.rb+1 −1 modified@@ -31,7 +31,7 @@ def destroy_resource_endorsement query = if @current_group.present? @resource.endorsements.where(decidim_user_group_id: @current_group&.id) else - @resource.endorsements.where(author: @current_user, decidim_user_group_id: nil) + @resource.endorsements.where(author: @current_user, decidim_user_group_id: 0) end query.destroy_all end
decidim-core/db/migrate/20231027142329_change_default_value_for_decidim_endorsements.rb+11 −0 added@@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ChangeDefaultValueForDecidimEndorsements < ActiveRecord::Migration[6.0] + def up + change_column_default :decidim_endorsements, :decidim_user_group_id, 0 + end + + def down + change_column_default :decidim_endorsements, :decidim_user_group_id, nil + end +end
decidim-core/lib/decidim/endorsable.rb+1 −1 modified@@ -21,7 +21,7 @@ def endorsed_by?(user, user_group = nil) if user_group endorsements.where(user_group: user_group).any? else - endorsements.where(author: user, user_group: nil).any? + endorsements.where(author: user, user_group: 0).any? end end end
decidim-core/lib/tasks/upgrade/decidim_deduplicate_endorsements.rake+53 −0 added@@ -0,0 +1,53 @@ +# frozen_string_literal: true + +namespace :decidim do + namespace :upgrade do + desc "Remove duplicated endorsements" + task fix_duplicate_endorsements: :environment do + logger = Logger.new($stdout) + logger.info("Removing duplicate endorsements...") + has_count = 0 + + columns = [:resource_type, :resource_id, :decidim_author_type, :decidim_author_id, :decidim_user_group_id] + + get_duplicates(columns).each do |issue| + while row_count(issue) > 1 + find_next(issue)&.destroy + has_count += 1 + logger.info("Removed duplicate endorsement for #{issue.resource_type} #{issue.resource_id}") + end + end + + logger.info("Patch remaining endorsements.") + Decidim::Endorsement.where(decidim_user_group_id: nil).update(decidim_user_group_id: 0) + logger.info("Process terminated, #{has_count} endorsements have been removed.") + logger.info("Done") + end + + private + + def get_duplicates(*columns) + Decidim::Endorsement.select("#{columns.join(",")}, COUNT(*)").group(columns).having("COUNT(*) > 1") + end + + def row_count(issue) + Decidim::Endorsement.where( + resource_type: issue.resource_type, + resource_id: issue.resource_id, + decidim_author_type: issue.decidim_author_type, + decidim_author_id: issue.decidim_author_id, + decidim_user_group_id: issue.decidim_user_group_id + ).count + end + + def find_next(issue) + Decidim::Endorsement.find_by( + resource_type: issue.resource_type, + resource_id: issue.resource_id, + decidim_author_type: issue.decidim_author_type, + decidim_author_id: issue.decidim_author_id, + decidim_user_group_id: issue.decidim_user_group_id + ) + end + end +end
decidim-core/spec/models/decidim/endorsement_spec.rb+3 −3 modified@@ -84,10 +84,10 @@ module Decidim create(:endorsement, resource: resource, author: author, user_group: other_user_group) end - it "sorts user_grup endorsements first and then by created_at" do + it "sorts user_group endorsements first and then by created_at" do expected_sorting = [ - endorsement.id, other_endorsement_2.id, - other_endorsement_1.id + other_endorsement_1.id, endorsement.id, + other_endorsement_2.id ] expect(resource.endorsements.for_listing.pluck(:id)).to eq(expected_sorting) end
7b840d2c37a5Fix duplicated endorsements (#11853)
7 files changed · +81 −5
decidim-core/app/commands/decidim/endorse_resource.rb+2 −0 modified@@ -31,6 +31,8 @@ def call else broadcast(:invalid) end + rescue ActiveRecord::RecordNotUnique + broadcast(:invalid) end private
decidim-core/app/commands/decidim/unendorse_resource.rb+1 −1 modified@@ -31,7 +31,7 @@ def destroy_resource_endorsement query = if @current_group.present? @resource.endorsements.where(decidim_user_group_id: @current_group&.id) else - @resource.endorsements.where(author: @current_user, decidim_user_group_id: nil) + @resource.endorsements.where(author: @current_user, decidim_user_group_id: 0) end query.destroy_all end
decidim-core/db/migrate/20231027142329_change_default_value_for_decidim_endorsements.rb+11 −0 added@@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ChangeDefaultValueForDecidimEndorsements < ActiveRecord::Migration[6.1] + def up + change_column_default :decidim_endorsements, :decidim_user_group_id, 0 + end + + def down + change_column_default :decidim_endorsements, :decidim_user_group_id, nil + end +end
decidim-core/lib/decidim/endorsable.rb+1 −1 modified@@ -21,7 +21,7 @@ def endorsed_by?(user, user_group = nil) if user_group endorsements.where(user_group:).any? else - endorsements.where(author: user, user_group: nil).any? + endorsements.where(author: user, user_group: 0).any? end end end
decidim-core/lib/tasks/upgrade/decidim_deduplicate_endorsements.rake+53 −0 added@@ -0,0 +1,53 @@ +# frozen_string_literal: true + +namespace :decidim do + namespace :upgrade do + desc "Remove duplicated endorsements" + task fix_duplicate_endorsements: :environment do + logger = Logger.new($stdout) + logger.info("Removing duplicate endorsements...") + has_count = 0 + + columns = [:resource_type, :resource_id, :decidim_author_type, :decidim_author_id, :decidim_user_group_id] + + get_duplicates(columns).each do |issue| + while row_count(issue) > 1 + find_next(issue)&.destroy + has_count += 1 + logger.info("Removed duplicate endorsement for #{issue.resource_type} #{issue.resource_id}") + end + end + + logger.info("Patch remaining endorsements.") + Decidim::Endorsement.where(decidim_user_group_id: nil).update(decidim_user_group_id: 0) + logger.info("Process terminated, #{has_count} endorsements have been removed.") + logger.info("Done") + end + + private + + def get_duplicates(*columns) + Decidim::Endorsement.select("#{columns.join(",")}, COUNT(*)").group(columns).having("COUNT(*) > 1") + end + + def row_count(issue) + Decidim::Endorsement.where( + resource_type: issue.resource_type, + resource_id: issue.resource_id, + decidim_author_type: issue.decidim_author_type, + decidim_author_id: issue.decidim_author_id, + decidim_user_group_id: issue.decidim_user_group_id + ).count + end + + def find_next(issue) + Decidim::Endorsement.find_by( + resource_type: issue.resource_type, + resource_id: issue.resource_id, + decidim_author_type: issue.decidim_author_type, + decidim_author_id: issue.decidim_author_id, + decidim_user_group_id: issue.decidim_user_group_id + ) + end + end +end
decidim-core/spec/models/decidim/endorsement_spec.rb+3 −3 modified@@ -84,10 +84,10 @@ module Decidim create(:endorsement, resource:, author:, user_group: other_user_group) end - it "sorts user_grup endorsements first and then by created_at" do + it "sorts user_group endorsements first and then by created_at" do expected_sorting = [ - endorsement.id, other_endorsement2.id, - other_endorsement1.id + other_endorsement1.id, endorsement.id, + other_endorsement2.id ] expect(resource.endorsements.for_listing.pluck(:id)).to eq(expected_sorting) end
RELEASE_NOTES.md+10 −0 modified@@ -285,6 +285,16 @@ bundle exec rails decidim:robots:replace You can see more details about this change on PR [\#11693](https://github.com/decidim/decidim/pull/11693) +### 3.12. Deduplicating endorsements + +We have identified a case when the same user can endorse the same resource multiple times. This is a bug that we have fixed in this release, but we need to clean up the existing duplicated endorsements. We have added a new task that helps you clean the duplicated endorsements. + +```bash +bundle exec rails decidim:upgrade:fix_duplicate_endorsements +``` + +You can see more details about this change on PR [\#11853](https://github.com/decidim/decidim/pull/11853) + ## 4. Scheduled tasks Implementers need to configure these changes it in your scheduler task system in the production server. We give the examples
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-r275-j57c-7mf2ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-47634ghsaADVISORY
- github.com/decidim/decidim/commit/5c5ee7a50d75c10643dd8c495e2517641e4d74dbghsaWEB
- github.com/decidim/decidim/commit/7b840d2c37a562709f4481db644d8c43add28536ghsaWEB
- github.com/decidim/decidim/releases/tag/v0.26.9ghsax_refsource_MISCWEB
- github.com/decidim/decidim/releases/tag/v0.27.5ghsax_refsource_MISCWEB
- github.com/decidim/decidim/releases/tag/v0.28.0ghsax_refsource_MISCWEB
- github.com/decidim/decidim/security/advisories/GHSA-r275-j57c-7mf2ghsax_refsource_CONFIRMWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/decidim/CVE-2023-47634.ymlghsaWEB
News mentions
0No linked articles in our index yet.