Rhino Security Labs

Ghost CMS Stored XSS Leading to Owner Takeover

Vulnerability Overview

Affected Product Summary

During research on the Ghost CMS application, the Rhino research team identified a Stored Cross-Site Scripting (XSS) vulnerability which can be triggered by a malicious profile image. This can be used for Ghost CMS instance takeover–providing an adversary with sole ownership of the Ghost CMS instance. With Owner privileges, the attacker’s account can no longer be deleted by other admins, they have full administrative access to the account and are able to modify billing details. 

The vendor does not view this as a valid vector so will not be releasing an official patch, but it’s important to us at Rhino to not release unpatched vulnerabilities. While this is a unique case, we’ve decided to make the patch ourselves which is available at https://github.com/TryGhost/Ghost/pull/19646

The proof-of-concept exploit can be found in our CVE Github repository: CVE-2024-23724: Stored XSS Leading to Owner Takeover in Ghost CMS

The unofficial patch for this CVE can be found in our pull request on the Ghost CMS repository: Pull Request #19646

Vendor: Ghost Foundation
Product: Ghost CMS
Confirmed Vulnerable Version: 5.76.0
Fixed Version: Pull Request #19646 (Open)
Product Link: https://ghost.org/

What is Ghost CMS?

Ghost CMS is a modern, open-source content management system designed for professional publishing. The focus on content creation and presentation makes it a popular choice among bloggers, journalists, and content creators. Ghost CMS is built on Node.js and offers a range of features including rich editor experiences, integrated SEO tools, native support for email newsletters, and membership capabilities. Ghost CMS can be self-hosted for free or hosted by the Ghost Foundation with a subscription. 

The Ghost CMS Docker image has over 100 million downloads, and is actively used by Apple, Mozilla, OpenAI, and other major brands.

CVE-2024-23724: Stored XSS in Profile Image

A primary focus of Ghost CMS is enabling collaboration among publishing staff. There are five levels of user roles in Ghost CMS, from least privileges (Contributor) to Owner: 

  1. Contributors: Can log in and write posts, but cannot publish. 
  2. Authors: Can create and publish new posts and tags. 
  3. Editors: Can invite, manage and edit authors and contributors. 
  4. Administrators: Have full permissions to edit all data and settings. 
  5. Owner: An admin who cannot be deleted and has access to billing details. 

To exploit this vulnerability, an adversary would need any user role on the system (including the low-privileged Contributor role) because each role can upload custom profile images. This can be done by navigating directly to the profile page and uploading the image.

The image above shows the profile picture upload page.

Embedding Cross-Site Scripting Payloads in SVGs

When applications accept SVG (Scalable Vector Graphics) files as profile pictures or for other user uploads, there is often the risk of Cross-Site Scripting (XSS) vulnerabilities. Unlike image formats such as PNG or JPG, SVGs are XML based and may include JavaScript (which could execute in the victim’s browser). While XSS risks in SVG uploads are common, they are not inherent–through techniques such as using DOMPurify to sanitize the file, developers can ensure these unique file formats are not abused. 

In Ghost CMS, SVG files are accepted as profile pictures and the application makes no attempt at sanitizing them. An adversary uploading a malicious SVG as a profile picture can execute JavaScript when the direct URL is viewed by another internal staff user. 

This can be exploited by uploading the following SVG:

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
   <rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" />
   <script type="text/javascript">

When another user navigates to the uploaded image, the Javascript successfully executes in their browser.

The image above shows the Javascript being executed in the user’s browser.

Weaponizing XSS: Ghost CMS instance Takeover

To demonstrate impact, we wanted to see what an adversary could do after compromising a user who has the low-privileged  “Contributor” role. In the Ghost CMS instance there can only be one “Owner”, who has management permissions over the instance. The Owner can transfer ownership to another user, but that user has to already be an Administrator. We created a custom SVG file that will do the following when the direct URL is viewed by the Owner of the Ghost CMS instance:

  1. Add the Administrator role to our user. 
  2. Transfer the Owner role to our (now Administrator) user. 

Once the Owner role has been transferred, our attacker user has unique privileges not available to other Administrators (and cannot be deleted by other Admins).

In order to craft a malicious SVG, we need the following information: 

  • User ID 
  • Username 
  • Slug Name 
  • User Email 
  • Admin Role ID (randomly updated per Ghost CMS instance) 

This information can be retrieved by navigating to the following URL after authenticating: http://[Ghost-CMS-Ghost CMS instance]/ghost/api/admin/users/?include=roles. This will return details on every staff user in the Ghost CMS instance, allowing us to identify the Admin Role ID needed for the attack (as well as our own user’s information). An example of this is in the image below.

The image above shows the user details from the /ghost/api/admin/users/ endpoint.

CVE-2024-23724 Proof of Concept

These details can be used to create an SVG file that executes JavaScript facilitating the Owner takeover. When the file is uploaded, the Ghost CMS instance provides a direct URL to view the SVG file. If the Owner of the instance navigates directly to this URL, it will lead to a full takeover of the Owner rule in the Ghost CMS instance. The boilerplate SVG code can be found on our Github

To simplify the creation of the malicious SVG, we have created a Python script that accepts arguments for the attacker’s username, password, and the URL of the Ghost CMS instance. It will then dynamically discover the information that is needed and output a malicious SVG to facilitate the attack. The image below shows the tool being used to generate the payload.

The clip above shows the Owner viewing the malicious SVG and the resulting changes to the attacker and victim accounts–once the Owner views the malicious link (SVG file), our attacker user becomes the Owner of the Ghost CMS instance. 

Vendor Response

We reported the vulnerability to Ghost CMS and they responded that it is not a valid vector and that “All [GhostCMS staff] users are expected to be trusted”.  

They elaborate on their security page (emphasis us), “A basic feature of Ghost as a CMS is to allow content creators to make use of scripts, SVGs, embedded content & other file uploads that are required for the content to display as intended. Because of this there will always be the possibility of ‘XSS’ attacks, albeit only from users that have been trusted to build the site’s content.” 

Since Ghost CMS is open-source software and encourages others to contribute to the code base on Github, we performed a code review and identified the vulnerable code located at /ghost/core/core/server/web/api/middleware/upload.js.

This middleware handles the file upload process for profile pictures. The middleware is not sanitizing the SVG files which leads to this XSS vulnerability.

Remediating CVE-2024-23724

The vendor does not consider this a valid vector and has decided to not release an official fix. However, it’s important to us at Rhino to not release details on unpatched vulnerabilities. While this is a unique case, we’ve decided to make the patch ourselves. 

We have submitted a pull request that uses DOMPurify to sanitize all SVG content and manually verified that this prevents CVE-2024-23724. DOMPurify works by taking a string of HTML and removing any potentially dangerous content that could be used in an XSS attack. It does so while keeping the sanitized HTML as close to the original input as possible, maintaining functionality and appearance without compromising security. 

Here’s a malicious SVG file before being sanitized by DOMPurify:

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
   <rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" />
   <script type="text/javascript">

This is the file after it is sanitized during the upload process. Note all of the JavaScript is removed, but it still retains the characteristics of color and height.

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
   <rect style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" height="100" width="300"></rect>


We found CVE-2024-23724 while researching the security of the Ghost CMS platform since it’s widely used by major brands. After discovering the possibility of Stored XSS in profile pictures, we increased the impact by demonstrating how an adversary could take over the “Owner” account in a Ghost CMS instance. While the vendor did not issue an official patch, we made a Pull Request so users can secure against this CVE themselves. 

We have added a proof-of-concept to our CVE Github repository that demonstrates this vulnerability. As always, feel free to follow us on Twitter or LinkedIn and join our Discord server for more releases and blog posts. 

Twitter: https://twitter.com/rhinosecurity

LinkedIn: https://www.linkedin.com/company/rhino-security-labs/ 

Discord: https://discord.gg/TUuH26G5

YouTube: https://youtube.com/@TylerRamsbey

Disclosure Timeline

12/08/2023 Issue reported to Ghost CMS.
12/11/2023 Response from Ghost CMS that they do not see it as a valid vector.
12/11/2023 We provided Ghost CMS with more information on how this can be used by an attacker.
01/02/2024 Followed up with Ghost CMS due to lack of reply.
01/04/2024 Response from Ghost CMS with the response that all staff users are expected to be trusted so they do not consider this a valid vector.
01/04/2024 Submitted vulnerability to Mitre with the vulnerability information and the reply from Ghost CMS, requesting a CVE.
1/24/2024 CVE is assigned by Mitre.
02/02/2024 Pull request submitted to Ghost CMS to patch the CVE.
2/13/2024 CVE publicly disclosed (blog post released)