skip to content
Back to GitHub.com
Home Bounties Research Advisories CodeQL Wall of Fame Get Involved Events
September 1, 2023

GHSL-2023-080: Unauthenticated data exfiltration in Decidim - CVE-2023-34090

Peter Stöckli

Coordinated Disclosure Timeline

Summary

Decidim, a platform for digital citizen participation, uses a third-party library named Ransack for filtering certain database collections (e.g., public meetings). By default, this library allows filtering on all data attributes and associations. This allowed an unauthenticated remote attacker to exfiltrate non-public data from the underlying database of a Decidim instance (e.g., exfiltrating data from the user table).

Product

Decidim

Tested Version

v0.27.2

Details

Data exfiltration due to misconfigured library (GHSL-2023-080)

Decidim uses Ransack for filtering certain database collections (e.g., public meetings). By default, Ransack allows filtering on all data attributes and associations.

Using a custom CodeQL query we discovered that user-controlled query filters can be passed into the filter parameter of in the CalendarsController:

def show
  render plain: CalendarRenderer.for(current_component, params[:filter]), content_type: "type/calendar"
end

This filter gets passed through several classes such as CalendarRenderer, BaseCalendar before ultimately ending up in a ransack sink in the ComponentCalendar class:

def filtered_meetings
  meetings.not_hidden.published.except_withdrawn.ransack(@filters).result
end

This allows a remote unauthenticated attacker to perform a brute force attack with specially crafted query filters. This attack is described in more detail in the blog post Ransacking your password reset tokens by Positive Security. While the attacker cannot directly retrieve data via queries, the attacker can retrieve contents of database fields by (mis)using Ransack’s start(of) method. Using this filter an attacker can find out if a given string starts with a given character. An attacker can now guess the values of a database field character by character.

Using the start(of) method the maximum number of requests required for brute forcing a string of the length n with a character set of size m is m*n. In contrast to cracking a password we don’t have to try every possible combination; instead we can brute force the first character (if the character is part of the lower-case alphabet we need 26 attempts in the worst case), then we can move on to brute force the second character while using the first character as a prefix and so on. Depending on the collation of the underlying database it might not be feasible for an attacker to exfiltrate case-sensitive values in an acceptable time frame. In the case of the following proof of concept where we exfiltrate a meeting salt with a length of 64 and a character set size of 16 (0-9a-f) we’re talking of 1024 (64 * 16) requests in the worst case.

Proof of Concept 1: Exfiltrating the secret salt of a meeting

In this proof of concept, we retrieve the secret salt of a meeting.

Precondition: At least one meeting with a salt must exist.

We know that for a given process we can retrieve all meetings in a calendar format using a link like this:

https://<host>/processes/facere-qui/f/11/calendar/

Note: a simple way to get such an URL for a process is to go to “Processes” and click on a process. On the process itself you click on “see all meetings”. Now you have a valid URL in the form of <host>/processes/<process-slug>/f/<number>/.

We can now add a Ransack filter to this URL:

curl "https://<host>/processes/facere-qui/f/11/calendar/?filter%5Bsalt_start%5D=0"

If this query still returns a result, we can assume there’s at least one meeting that has a salt starting with 0. If it doesn’t return anything anymore, we can assume that there’s no meeting that has a salt starting with 0 and continue with the rest of the character set (0-9a-f). Within 16 requests we know the first character of a meeting salt. We can now query for the rest of the characters by adding more characters to the query filter. Using a script could speed up this process dramatically.

Proof of Concept 2: Exfiltrating the email address of a user

In this proof of concept, we exfiltrate an email address of a user that is participating in said meeting (via the user table).

Precondition: At least one meeting with a user registration must exist.

The data model for a Meeting is associated with the user table via its registrations attribute. This allows us to (mis)use this connection to retrieve the email address of a registered user, using a link such as:

curl "https://<host>/processes/facere-qui/f/11/calendar/?filter%5Bregistrations_user_email_start%5D=m"

. . . until we are in possession of the full email address.

curl "https://<host>/processes/facere-qui/f/11/calendar/?filter%5Bregistrations_user_email_start%5D=meeting-registered-user-14-1@example.org"

Sidenote: In the case of brute forcing an email address an attacker has to work with a bigger character set (e.g. 0-9a-z.-_@ = 40) compared to the case with the salt. However, this is only true for the local-part of an email address. In practice the domain part doesn’t have to be completely brute forced as many users use the same mail provider (e.g., gmail.com, hotmail.com, etc.). This largely reduces the numbers of requests required. E.g., in the case where the local-part + @ sign of a gmail.com address is of length 10.

Impact

This issue may lead to Sensitive Data Disclosure.

Resources

CVE

Credit

This issue was discovered and reported by GHSL team member @p- (Peter Stöckli).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2023-080 in any communication regarding this issue.