VYPR
Low severityNVD Advisory· Published Feb 20, 2024· Updated Aug 2, 2024

Decidim has race condition in Endorsements

CVE-2023-47634

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.

PackageAffected versionsPatched versions
decidimRubyGems
>= 0.10.0, < 0.26.90.26.9
decidimRubyGems
>= 0.27.0, < 0.27.50.27.5

Affected products

1

Patches

2
5c5ee7a50d75

Backport 'Fix duplicated endorsements' to v0.26 (#11973)

https://github.com/decidim/decidimAndrés Pereira de LucenaNov 10, 2023via ghsa
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
    
7b840d2c37a5

Fix duplicated endorsements (#11853)

https://github.com/decidim/decidimAlexandru Emil LupuNov 9, 2023via ghsa
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

News mentions

0

No linked articles in our index yet.