CVE-2026-54002: Kirby CMS: stored XSS bypasses DOM sanitizer via unwrap flaw

GHSA-wr9h-4r83-f4v6 HIGH
Published June 18, 2026
CISO Take

Kirby CMS's Dom::sanitize() method fails to re-sanitize child nodes after unwrapping unknown HTML/XML parent tags, allowing any authenticated panel user — even low-privileged editors — to permanently store JavaScript that fires automatically in the browsers of admins and other users who view the content. With 479 downstream dependents and 35 prior CVEs in this package, the ecosystem footprint is meaningful, and the privilege-escalation path (editor → admin session hijack via stored XSS) is a well-understood, trivial-to-exploit class of attack. No public exploit or CISA KEV entry exists at this time, and exploitation requires an attacker to already hold a valid Kirby panel account, moderating the urgency somewhat. Upgrade to Kirby 4.9.4 or 5.4.4 immediately and, on security-critical sites, manually review or re-sanitize all previously stored content in writer and list fields, since pre-patch content may still contain injected payloads.

Sources: GitHub Advisory NVD OpenSSF ATLAS

What is the risk?

High severity in any multi-user Kirby deployment where editor-level access is granted to partially-trusted users. The authentication prerequisite limits opportunistic exploitation, but insider threats and compromised editor credentials remain realistic vectors. The absence of CVSS data, EPSS, and public exploits reduces measured urgency, though the vulnerability class (stored XSS enabling admin session hijack) is well-understood and requires no specialized tooling. OpenSSF scorecard 6.6/10 and 35 historical CVEs in getkirby/cms indicate persistent security debt in this package.

How does the attack unfold?

Initial Access
Attacker authenticates as a low-privileged Kirby panel editor using valid credentials and navigates to a writer or list field in the content editor.
AML.T0012
Sanitization Bypass
Attacker crafts markup containing a JavaScript payload nested inside an unknown HTML/XML tag; Dom::sanitize() unwraps the parent but skips sanitizing its children, storing the malicious script verbatim.
AML.T0049
Payload Detonation
An admin or higher-privileged user views the poisoned content in the Panel or site frontend, triggering automatic execution of the stored JavaScript in their browser session.
AML.T0011
Privilege Escalation
The injected script exfiltrates the admin's session token or abuses the Kirby REST API under admin permissions to create backdoor accounts, exfiltrate user data, or alter published AI governance content.
AML.T0106

What systems are affected?

Package Ecosystem Vulnerable Range Patched
Panel composer <= 4.9.3 4.9.4
5.7K OpenSSF 6.6 479 dependents Pushed 5d ago 60% patched ~5d to patch Full package profile →

Do you use Panel? You're affected.

How severe is it?

CVSS 3.1
N/A
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

What should I do?

5 steps
  1. Patch: upgrade getkirby/cms to 4.9.4 (Kirby 4.x branch) or 5.4.4 (Kirby 5.x branch) immediately.

  2. Post-patch content remediation: re-sanitize or manually audit all writer and list field content stored before the patch — the fixed Dom::unwrap() now moves (not clones) child nodes, so re-running sanitization will catch previously escaped payloads.

  3. If patching is blocked: temporarily restrict writer/list field write access to fully trusted users only via panel role configuration.

  4. Audit plugin and custom code for any direct calls to Dom::sanitize(), Sane::sanitize(), or $file->sanitizeContents() with untrusted input.

  5. Harden server-side CSP headers (script-src 'self' at minimum) to reduce XSS blast radius during the patching window.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Article 9 - Risk Management System
ISO 42001
A.9.2 - Information security controls for AI systems
NIST AI RMF
GOVERN 1.7 - Processes and procedures for AI risk management
OWASP LLM Top 10
LLM09 - Misinformation

Frequently Asked Questions

What is CVE-2026-54002?

Kirby CMS's Dom::sanitize() method fails to re-sanitize child nodes after unwrapping unknown HTML/XML parent tags, allowing any authenticated panel user — even low-privileged editors — to permanently store JavaScript that fires automatically in the browsers of admins and other users who view the content. With 479 downstream dependents and 35 prior CVEs in this package, the ecosystem footprint is meaningful, and the privilege-escalation path (editor → admin session hijack via stored XSS) is a well-understood, trivial-to-exploit class of attack. No public exploit or CISA KEV entry exists at this time, and exploitation requires an attacker to already hold a valid Kirby panel account, moderating the urgency somewhat. Upgrade to Kirby 4.9.4 or 5.4.4 immediately and, on security-critical sites, manually review or re-sanitize all previously stored content in writer and list fields, since pre-patch content may still contain injected payloads.

Is CVE-2026-54002 actively exploited?

No confirmed active exploitation of CVE-2026-54002 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-54002?

1. Patch: upgrade getkirby/cms to 4.9.4 (Kirby 4.x branch) or 5.4.4 (Kirby 5.x branch) immediately. 2. Post-patch content remediation: re-sanitize or manually audit all writer and list field content stored before the patch — the fixed Dom::unwrap() now moves (not clones) child nodes, so re-running sanitization will catch previously escaped payloads. 3. If patching is blocked: temporarily restrict writer/list field write access to fully trusted users only via panel role configuration. 4. Audit plugin and custom code for any direct calls to Dom::sanitize(), Sane::sanitize(), or $file->sanitizeContents() with untrusted input. 5. Harden server-side CSP headers (script-src 'self' at minimum) to reduce XSS blast radius during the patching window.

What systems are affected by CVE-2026-54002?

This vulnerability affects the following AI/ML architecture patterns: AI governance documentation portals, ML model card publishing platforms, Multi-user AI tool admin interfaces, AI threat intelligence content management systems.

What is the CVSS score for CVE-2026-54002?

No CVSS score has been assigned yet.

What is the AI security impact?

Affected AI Architectures

AI governance documentation portalsML model card publishing platformsMulti-user AI tool admin interfacesAI threat intelligence content management systems

MITRE ATLAS Techniques

AML.T0011 User Execution
AML.T0012 Valid Accounts
AML.T0049 Exploit Public-Facing Application
AML.T0106 Exploitation for Credential Access

Compliance Controls Affected

EU AI Act: Article 9
ISO 42001: A.9.2
NIST AI RMF: GOVERN 1.7
OWASP LLM Top 10: LLM09

What are the technical details?

Original Advisory

### TL;DR This vulnerability affects Kirby sites and plugins that use the `writer` or `list` fields or that use `$dom->sanitize()`, `Sane::sanitize()`, `Sane\Html::sanitize()`, `Sane\Svg::sanitize()`, `Sane\Xml::sanitize()`, `Sane::sanitizeFile()` or `$file->sanitizeContents()` with untrusted input. It was possible to inject malicious markup as children of an unknown HTML/XML tag, which would then be passed through `Dom::sanitize()` without being correctly sanitized according to the provided sanitization rules, causing a cross-site scripting (XSS) risk. **This vulnerability is of high severity for affected sites.** The default file upload protection is *not* affected, so sites that only *validate* uploaded files are not exposed to this vulnerability. The vulnerability can only be exploited by authenticated users. ---- ### Introduction Cross-site scripting (XSS) is a type of vulnerability that allows executing any kind of JavaScript code inside the site frontend or Panel session of the same or other users. In the Panel, a harmful script can, for example, trigger requests to Kirby's API with the permissions of the victim. In a *stored* XSS attack, the malicious payload is saved into the content data and has the potential to affect other users or site visitors. Such vulnerabilities are critical if you might have potential attackers in your group of authenticated Panel users. They can escalate their privileges if they get access to the Panel session of an admin user. Depending on your site, other JavaScript-powered attacks are possible. A specific class of stored XSS is auto-firing, meaning the maliciously injected JavaScript code is executed by the browser when the page loads without the victim having to perform a specific action. ### Affected components The `Dom::sanitize()` method allows removing unwanted or malicious elements or attributes from DOM documents (which includes HTML, SVG or arbitrary XML data). Sanitized content is supposed to be protected against cross-site scripting (XSS) attacks by removing their impact from untrusted content input. `Dom::sanitize()` internally checks all nodes and attributes of the DOM document. It removes nodes, attributes, processing instructions, doctypes and namespaces that are not allowed according to the provided configuration. Nodes with tags that have been explicitly blocklisted are removed together with their children. Nodes with explicitly allowlisted tags are kept. If a tag is neither allowlisted nor blocklisted, nodes with that tag are unwrapped by `Dom::unwrap()` (meaning the children are kept). `Dom::sanitize()` is used in the following components: - The `writer` field sanitizes all entered content on the backend before it is saved to the content file. - The `list` field performs the same sanitization as the `writer` field since Kirby 5.4.1 (and in backported versions since Kirby 4.9.1). - The `Kirby\Sane` class package includes higher-level classes `Sane\Html`, `Sane\Svg` and `Sane\Xml` that all rely on DOM sanitization. - These classes are in turn also used by `Sane::sanitizeFile()` and `$file->sanitizeContents()`. - Any of the mentioned methods could also be used in site or plugin code, for example in a `file.create:before` hook that cleans uploaded SVG/HTML files. Only the sanitization path (returning a cleaned document) is affected. The validation path is *not* affected by this vulnerability. Kirby's default upload protection performs validation, so malicious SVG or HTML uploads continue to be rejected. ### Impact In affected releases, `Dom::sanitize()` did not sanitize nodes that had been unwrapped from their parent node. The affected child nodes would be copied into the resulting sanitized document without being sanitized. An authenticated Panel user who can edit a `writer` or `list` field can store markup that survives sanitization and executes as JavaScript when the content is rendered, both in the Panel and on the site frontend. This allows a lower-privileged editor to run scripts in the context of higher-privileged users (for example admins) who view the content ("stored XSS"). Where a plugin or custom code cleans uploaded SVG/HTML with the `Sane` API, the same flaw leaves active content in the stored file, which executes when the file is served. ### Patches The problem has been patched in [Kirby 4.9.4](https://github.com/getkirby/kirby/releases/tag/4.9.4) and [Kirby 5.4.4](https://github.com/getkirby/kirby/releases/tag/5.4.4). Please update to one of these or a [later version](https://github.com/getkirby/kirby/releases) to fix the vulnerability. In all of the mentioned releases, `Dom::unwrap()` now *moves* the allowed children to the parent instead of cloning them, so the exact nodes remain in the document and are covered by the sanitization pass. Note that content that was passed through the sanitizer and stored as field content before the patch may contain malicious content that was not properly sanitized due to the vulnerable code. If you cannot rule out attackers under the authenticated users of a security-critical site, we advise reviewing the content for possible attacks or to re-sanitize all content of affected fields. ### Credits Thanks to Shafiq Aiman (@shafiqaimanx) for responsibly reporting the identified issue.

Exploitation Scenario

An adversary holding a low-privileged Kirby editor account on an AI governance documentation portal crafts a writer field entry containing a JavaScript payload nested inside a non-standard or unknown HTML element (e.g., a custom tag like <ai-widget>). During save, Dom::sanitize() encounters the unknown tag, unwraps it via Dom::unwrap(), and copies the children into the output document without running them through the sanitization pass — so the malicious script persists. The next time a Kirby panel admin reviews the content, the script executes silently in their browser, exfiltrates their session cookie to an attacker-controlled endpoint, and the adversary uses the hijacked admin session to create a backdoor account, modify published content, or extract user data via Kirby's REST API.

Weaknesses (CWE)

CWE-79 — Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'): The product 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.

  • [Architecture and Design] Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid [REF-1482]. Examples of libraries and frameworks that make it easier to generate properly encoded output include Microsoft's Anti-XSS library, the OWASP ESAPI Encoding module, and Apache Wicket.
  • [Implementation, Architecture and Design] Understand the context in which your data will be used and the encoding that will be expected. This is especially important when transmitting data between different components, or when generating outputs that can contain multiple encodings at the same time, such as web pages or multi-part mail messages. Study all expected communication protocols and data representations to determine the required encoding strategies. For any data that will be output to another web page, especially any data that was received from external inputs, use the appropriate encoding on all non-alphanumeric characters. Parts of the same output document may require different encodings, which will vary depending on whether the output is in the: etc. Note that HTML Entity Encoding is only appropriate for the HTML body. Consult the XSS Prevention Cheat Sheet [REF-724] for more details on the types of encoding and escaping that are needed. HTML body Element attributes (such as src="XYZ") URIs JavaScript sections Casca

Source: MITRE CWE corpus.

Timeline

Published
June 18, 2026
Last Modified
June 18, 2026
First Seen
June 18, 2026

Related Vulnerabilities