Hotanya Ragtah
writing/advisory

Stored XSS via SVG Uploads in Backdrop CMS

Two ways to get JavaScript past the SVG upload check in Backdrop CMS, one with a tab character and one with an SVG animate element. Both lead to stored XSS, fixed in 1.35.2 and 1.34.6.

Hotanya Ragtah6 min readxsssvgbackdrop-cmsopen-sourceappsec
CVE
CVE-2026-111442
Project
Backdrop CMS
Component
file_validate_svg()
Status
Fixed · 1.35.2 / 1.34.6
A dark advisory cover reading "Backdrop CMS — Stored XSS via SVG Uploads", labelled CVE-2026-111442, fixed in 1.35.2 / 1.34.6

Earlier this year I spent some time looking at Backdrop CMS, an open source CMS that started as a fork of Drupal 7. Its official Docker image has more than 8 million pulls on Docker Hub, so it gets used a fair bit. I found two ways to get JavaScript past the check Backdrop uses to block dangerous SVG uploads. Both lead to stored cross-site scripting (XSS).

I reported both to the Backdrop security team in April 2026. The fix shipped in October as part of BACKDROP-SA-CORE-2026-007, and the issue was given CVE-2026-111442 with a CVSS 4.0 score of 5.4.

If you run Backdrop, update to 1.35.2 or 1.34.6 (or later).

Some Background

SVG files are images, but they are also XML documents, and they can contain links, scripts and animations. If a site lets people upload SVGs and then serves them from its own domain, a malicious SVG can run JavaScript as if it were part of the site.

Backdrop already has some protection for this. Before it accepts an SVG, it runs the file through a function called file_validate_svg() in core/includes/file.inc. The function loads the SVG and runs a list of XPath queries over it. If any query finds something, the upload is rejected. The list looks for things like:

//svg:script
//svg:foreignobject
//svg:set
//@*[starts-with(name(), "on")]
//svg:a[starts-with(normalize-space(@href), "javascript:")]

So <script> tags are out, onload= style event handlers are out, and links that start with javascript: are out. That covers the obvious cases. Both of my findings are about the less obvious ones.

For testing, I ran Backdrop 1.33.1 in Docker on my laptop.

Fig. 1Backdrop status report showing version 1.33.1, with 1.35.2 available as a security update

As a quick check, here is what happens when a user with the editor role uploads an SVG with a plain javascript: link. Backdrop catches it:

Fig. 2An SVG with a plain javascript: link rejected with "Dangerous content found"

Finding 1: Hiding javascript: with a Tab Character

The Problem

Here is the check for javascript: links again:

//svg:a[starts-with(normalize-space(@href), "javascript:")]

normalize-space() is an XPath 1.0 function that tidies up whitespace. It trims spaces from the start and end of a string, and it turns any run of whitespace in the middle (spaces, tabs, newlines) into a single space.

That second part is the problem. If you put a tab in the middle of the word javascript, like this:

jav&#x09;ascript:alert(1)

Backdrop decodes &#x09; into a real tab before it checks the file, and by the time the XPath query runs, that tab has become a space. The string it checks is now:

jav ascript:alert(1)

That does not start with javascript:, so the check passes and the file is accepted.

The browser reads the same link differently. The URL standard says tabs and newlines are removed from a URL before it is parsed, so the browser sees javascript:alert(1) and runs it when the link is clicked.

So the validator and the browser disagree about what the link says, and the attacker gets to use that gap.

Proof of Concept

  1. Log in as a user who can upload images. I used the file picker on the page edit screen.
  2. Upload this SVG:
xml
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <a href="jav&#x09;ascript:console.log('XSS Triggered: ' + document.location)">
    <text x="10" y="20">Click to Verify Security Patch</text>
  </a>
</svg>
  1. Open the uploaded file directly by its URL.
  2. Click the text.
  3. The message shows up in the browser console, running on the site’s own domain.
Fig. 3The console.log payload firing after clicking the link in the uploaded SVG (subdomain blacked out)

Swapping console.log for alert(document.domain) makes it easier to see. You can also see the browser’s status bar at the bottom showing where the link really goes: javascript:alert(document.domain), with the tab gone.

Fig. 4The tab payload firing alert(document.domain), with the status bar showing the real link target

The Problem

SVG has a set of animation elements: <set>, <animate>, <animateTransform> and <animateMotion>. They can change an element’s attributes while the image is being shown. That includes the href on a link.

Backdrop already blocks <set> for this reason. But <animate> can do the same job, and it was not on the list.

That means you can upload a link with no href at all, or a harmless one, and let <animate> fill in a javascript: URL once the browser starts showing the image. The validator only ever sees the file as it was uploaded, so it never sees the bad link.

Proof of Concept

  1. Log in as a user who can upload images.
  2. Upload this SVG:
xml
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <a>
    <animate attributeName="href" values="javascript:alert(document.domain)" begin="0s"/>
    <text y="20">click me</text>
  </a>
</svg>

Backdrop accepts it without complaint:

Fig. 5Backdrop accepting the SVG that uses animate to set the link
  1. Open the uploaded file directly by its URL.
  2. Click the text.
  3. An alert box pops up showing the site’s domain.
Fig. 6The animate payload firing alert(document.domain)

The same trick works with attributeName="xlink:href".

A note on credit: another researcher had reported this one to Backdrop a few days before I did, so I am credited as an additional reporter for it.

The Impact

Both findings give you stored XSS, but there are a few limits:

  • The attacker needs an account that can upload SVGs. On a standard Backdrop install, the editor role can do this.
  • The victim has to open the SVG file directly, for example by following a link to it. When Backdrop shows an uploaded SVG inside a page, it uses an <img> tag, and browsers do not run scripts in images loaded that way.
  • The victim has to click the link inside the SVG. This is not a zero-click attack.

If those conditions are met, the attacker’s JavaScript runs on the site’s own domain. If the person who clicks is an administrator, the script can do anything that admin can do, such as creating a new admin account. The file stays on the site until someone deletes it, so it can catch more than one person.

Backdrop rated the advisory as Critical using its own risk calculator. The CVE has a CVSS 4.0 score of 5.4 (CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:N/SC:L/SI:L/SA:N). The two systems score things differently, so the numbers don’t line up.

Mitigation and Remediation

The real fix is to update Backdrop to 1.35.2 or 1.34.6 or later. If you are building your own SVG upload check, here is what I recommended to Backdrop:

  1. Use an allowlist instead of a blocklist.

    A blocklist has to guess every dangerous element and attribute, and these two findings show how easy it is to miss one. It is safer to list the SVG elements and attributes you allow and remove everything else.

  2. Don’t check URLs with XPath string matching.

    Pull the attribute value out in your own code, strip out control characters and whitespace, and then check it. For example in PHP:

    $clean = preg_replace('/[\x00-\x20]+/', '', $value);

    Then compare it against a short list of schemes you allow, such as http, https and mailto. Backdrop already has a helper for this, backdrop_strip_dangerous_protocols().

  3. Block all of the animation elements, not just one.

    If you block <set>, you also need to block <animate>, <animateTransform> and <animateMotion>. Anything that can change href or xlink:href while the image is showing should be treated as unsafe.

  4. Serve uploaded SVGs safely.

    Sending uploaded files with Content-Disposition: attachment, X-Content-Type-Options: nosniff and a strict Content-Security-Policy, or serving them from a separate domain, stops a bad file from running even if it gets past the upload check.

Thanks

Thanks to Jen Lampton and the Backdrop security team for a fast first reply, clear communication, and for crediting everyone who reported these issues.

Hotanya Ragtah is a Senior AppSec Engineer. More about me or get in touch.

esc