Severity Daily

IT and AI security incidents, checked against the primary source

Tag: CVE-2026-90561

  • Strapi fixed the WYSIWYG preview XSS in June; the CVE arrived Sunday, and the end-of-life 4.x branch still ships the flaw

    Strapi fixed the WYSIWYG preview XSS in June; the CVE arrived Sunday, and the end-of-life 4.x branch still ships the flaw

    CVE-2026-90561 was published Sunday against a fix that reached npm on June 17, 2026 — and against a 4.x branch that went end-of-life in April and still ships the vulnerable code.

    What happened

    NVD published CVE-2026-90561 on September 13, 2026 at 11:17 a.m. UTC. The record is assigned by VulnCheck, carries a CVSS v3.1 base score of 8.7 and the vector CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N, a secondary CVSS v4.0 score of 9.3, and CWE-79. Its vulnStatus was still Received when this story was checked, meaning NVD has not completed its own analysis.

    The description reads, in full: “Strapi versions 4.x through 4.26.2 and 5.x before 5.48.1 contain a stored cross-site scripting vulnerability in the content manager WYSIWYG preview component that fails to strip script tags from rich text.”

    The record points at a specific file, and the file explains the flaw exactly. In Strapi 5.46.0, PreviewWysiwyg.tsx renders markdown and then hands it to the sanitize-html library before writing it into the page through React’s dangerouslySetInnerHTML. The sanitizer call looks careful. It spreads in the library defaults and then supplies an attribute allowlist naming href, align, alt, width, height, src and a handful of others. Between those two lines sits a third: allowedTags: false.

    In sanitize-html, allowedTags: false does not mean “allow no tags.” It disables tag filtering altogether. Every tag passes, including <script>. The attribute allowlist below it still runs, which is why the code reads as protective to anyone scanning it quickly — the restrictive-looking part is real, and it is guarding the wrong thing.

    The fix, in commit 8757526, deletes the sanitize-html call and replaces it with a local sanitize() helper built on DOMPurify. The helper reproduces the old attribute allowlist, restricts URL schemes to http, https, ftp, mailto and tel plus relative forms, and registers a hook stripping data: URIs, which DOMPurify otherwise permits on media tags through an internal allowlist that bypasses the scheme check. Tag filtering is now DOMPurify’s default allowlist, which does not include <script>.

    That fix is in 5.48.1. The npm registry records 5.48.1 as published on June 17, 2026 — two months and twenty-seven days before the CVE ID existed. The current release is 5.53.0, published September 9, 2026. Anyone on a reasonably current Strapi 5 has had the fix since midsummer without knowing it was a fix.

    The 4.x branch

    The affected range names 4.x through 4.26.2 and stops there. It names no fixed 4.x version, because there is not one.

    Severity Daily downloaded @strapi/admin 4.26.2 from the npm registry and searched the shipped bundle rather than the repository. The compiled chunk dist/_chunks/index-Cwr9u7_W.mjs contains the PreviewWysiwyg component with the same construction: sanitizeHtml(md.render(...), { ...sanitizeHtml.defaults, allowedTags: false, allowedAttributes: {...} }), and the output placed into dangerouslySetInnerHTML. The code in the published artifact is the vulnerable code, character for character in the parts that matter.

    4.26.2 is the newest 4.x release. npm published it on June 9, 2026, and it currently carries the legacy dist-tag, which is what an installer resolves for anyone who asks for the old major by name rather than by number.

    Strapi said in advance that this would happen. A notice posted by a Strapi maintainer on August 25, 2025 states that Strapi 4 “will be ending it’s support coverage at the end of October 2025 and will move into a 6 month security only maintenance period,” and that “Security fixes will be provided until the end of April 2026, at which point Strapi 4 will be considered EOL (End-Of-Life).” The issue behind this CVE, according to the public GitHub issue, was reported privately on May 18, 2026 — eighteen days after that date. Strapi was under no stated obligation to fix it in 4.x, and did not.

    Why it matters

    The absence of a 4.x fix is policy working as written. What is worth attention is that nothing in the machine-readable record says so.

    A vulnerability record has two ways to describe a version range with no patch. It can mean “a fix is coming and has not landed yet,” or it can mean “this branch is dead and the remedy is to leave it.” Those demand opposite responses from the person holding the alert, and CVE records do not distinguish them. An operator running Strapi 4.26.2 who sees this CVE come through a scanner gets an affected range, a high score, and no fixed version — the same output they would get for a zero-day awaiting a vendor patch. The instruction they actually need, that the branch reached end-of-life in April and the upgrade is a major-version migration rather than a patch bump, exists only in a changelog post from August 2025.

    The second thing is the interval. This is the third story on this site in three days in which the fix and the CVE identifier are separated by weeks or months. Flowise got CVE IDs six weeks after 3.1.4 closed the hole; ESPnet’s patched path required a PyTorch floor its own metadata did not declare. Strapi is the widest of the three: a fix shipped in June inside an ordinary point release, no advisory, no GitHub Security Advisory, and an identifier in September. Strapi has published GHSA entries this year — GHSA-rjg2-95×7-8qmx in May and GHSA-93c7-w42j-xh4p in June — so the absence of one here is a choice about this issue rather than a project that does not use the mechanism.

    The flaw itself is worth understanding for a reason beyond Strapi. The sanitizer was present, was called on every render, and was configured by someone who was thinking about security — the attribute allowlist is evidence of that. The single option that disabled the tag filter is a falsy value in a configuration object, the kind of line that reads as a tightening rather than a loosening. A reviewer looking for “is this output sanitized” would have found a sanitizer and moved on. That failure mode does not show up in dependency scanning, which sees a current sanitize-html and reports no finding.

    The privilege model makes the score what it is. The vector requires low privileges to store the payload, user interaction to fire it, and changes scope: an Author-level account writes the payload into a rich text field and a higher-privileged account — an Editor or a Super Admin — executes it by opening the preview. The reporter’s public issue characterizes the outcome as takeover of Editor and Super Admin accounts through stolen session material. That is the reporter’s characterization; Strapi has published no assessment of impact, because Strapi has published nothing about this issue at all.

    What to do

    On Strapi 5, upgrade to 5.48.1 or later. If you are on 5.53.0, published September 9, you already have the fix and no action is required. If you are below 5.48.1, this is an ordinary point-release upgrade within the same major.

    On Strapi 4, there is no version to move to. 4.26.2 is the end of the line and it ships the vulnerable component. The remedy is the v4-to-v5 migration, which is not a patch. Until that is done, the exposure is bounded by who can write rich text: audit which accounts hold Author or higher on collections with rich text fields, and treat the preview pane as untrusted rendering surface. Narrowing authoring rights on those collections reduces the set of people who can plant the payload, which is the only control available short of migrating.

    On either branch, the check is direct. Look at the @strapi/admin version in your lockfile, not at the version you believe you deployed.

    Sourcing note

    Checked: the NVD record for CVE-2026-90561, read from the NVD REST API, for the description, scores, vectors, CWE, affected ranges, timestamps and status. The Strapi source at tags v5.46.0, v5.48.1 and v5.53.0, read from raw file retrieval, for the vulnerable call and its replacement. The npm registry for the publication dates of 5.48.1, 5.53.0 and 4.26.2 and for the current dist-tags. The published @strapi/admin 4.26.2 tarball, downloaded from npm and searched directly, to confirm the shipped artifact contains the vulnerable code rather than inferring it from the repository. The Strapi maintainer notice of August 25, 2025 for the v4 support and end-of-life dates, quoted exactly. GitHub’s advisory listing for Strapi, which shows no GHSA corresponding to this CVE.

    Not independently confirmed: the private report date of May 18, 2026 and the public issue date of June 30, 2026 come from the GitHub issue text itself, not from a Strapi statement. The issue is shown as closed as not planned and carries an automation label reading “flag: invalid template”; no closure date and no maintainer comment were visible on the page, and the fix in 5.48.1 predates the public issue by thirteen days, so the closure should not be read as a dismissal of the substance. The reporter’s account-takeover impact claim is the reporter’s, unverified here.

    Unresolved: Strapi has issued no advisory, no changelog security note, and no statement about 4.x. Whether the 5.48.1 release notes mention the change at all was not established. cisa.gov returns 403 to automated retrieval and was not consulted; this CVE carries no CISA fields in NVD and no federal deadline. No exploitation has been reported by anyone, and none is claimed here.