Coordinated Disclosure Timeline
- 2023-04-11: Reported via Private Vulnerability Reporting (PVR)
- 2023-04-20: The Decidim team created a pull request to fix the data exfiltration vulnerability.
- 2023-05-11: New Decidim releases were published that fix this and other security vulnerabilities.
- 2023-07-28: A blog post is released by GitHub Security Lab with more details about this vulnerability.
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
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
- CVE-2023-34090
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.