Overview

a stored xss in silverstripe page breadcrumbs

June 24, 2026
3 min read

TL;DR

I got CVE-2026-54717 in SilverStripe CMS.

This one was a stored XSS in the CMS admin interface, specifically in the page list view breadcrumbs.

The vulnerable area was:

/admin/pages/listview

An attacker with permission to create or edit pages could place HTML or JavaScript in a parent page title or menu title, then get that payload rendered inside the breadcrumb trail for child pages.

So the bug was not in the current page title itself.

It was in the ancestor breadcrumb path.

The bug

The report path was pretty straightforward.

When the page list view was built, ancestor page titles were gathered and concatenated into a breadcrumb string:

$pages = $item->getAncestors();
$pageNames = [];
foreach ($pages as $page) {
$pageNames[] = $page->MenuTitle ?: $page->Title;
}
$breadcrumbs = implode('/', $pageNames);
return $title . sprintf(
'<p class="small cms-list__item-breadcrumbs">%s</p>',
$breadcrumbs
);

That is the whole issue.

The current page title had already gone through the safer rendering path, but the ancestor titles used for the breadcrumb string were inserted into an HTML fragment without proper escaping first.

So if an attacker controlled one of those parent titles, the breadcrumb renderer would happily turn it back into live HTML in the CMS interface.

Why it worked

The attacker-controlled value could come from either:

  • MenuTitle
  • or fallback Title

And because the breadcrumb string was built with implode('/') and then interpolated directly into:

<p class="small cms-list__item-breadcrumbs">%s</p>

the output path treated the whole breadcrumb string as already-safe HTML.

That is exactly the kind of bug that feels small in code review until you remember that page titles are just user-controlled content with a nicer label.

Repro shape

The payload from the report was simple:

<img src=x onerror=alert(1)>

The repro flow was:

  1. log in as a CMS user who can create or edit pages
  2. create or edit a parent page
  3. set Title or MenuTitle to the payload above
  4. create a child page under that parent
  5. visit the list view for that parent

Example target:

/admin/pages/listview?ParentID=<parentID>

At that point, the breadcrumb trail would render the attacker-controlled HTML and execute it in the CMS admin context.

Why I liked it

I like this kind of stored XSS because it hides in a UI detail that people tend to trust too much.

Breadcrumbs look decorative. They feel like a harmless secondary label.

But once that breadcrumb is built from page titles inside the authenticated CMS interface, it becomes a very real execution sink.

And the privilege boundary is good too:

  • lower-privileged content editor controls page title
  • higher-privileged CMS user opens the list view
  • payload executes in the victim admin origin

That is a clean stored-XSS story.

Impact

The original report angle was the one I care about most here: editor-to-admin abuse inside the CMS.

Once JavaScript runs in that origin, the attacker can potentially:

  • execute privileged CMS actions on behalf of the victim
  • modify or publish content
  • access same-origin admin data
  • exfiltrate CSRF tokens
  • forge follow-up requests

The public advisory ended up describing the impact more narrowly as:

Page breadcrumbs in the CMS are vulnerable to XSS when viewed using the page list view

That is still the same bug, just phrased more conservatively.

Public advisory status

The public GHSA now lists:

  • CVE: CVE-2026-54717
  • Severity: Moderate
  • CVSS: 5.4
  • Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
  • Affected: versions before 6.2.1
  • Patched: 6.2.1

That is a bit more conservative than how I initially thought about the issue during reporting, but the underlying bug is still the same stored XSS in authenticated CMS workflow.

Fix direction

The fix direction is also the obvious one.

Any user-controlled value included in the breadcrumb output should be escaped before it is inserted into the HTML fragment.

In SilverStripe terms, that means using proper output encoding such as:

Convert::raw2xml()

or the equivalent escaping path before rendering the breadcrumb string.

Reference

Thanks for reading this blog post all the way to the end