Piranha CMS - Stored XSS in Page Title
Description
In PiranhaCMS, versions 7.0.0 to 9.1.1 are vulnerable to stored XSS due to the page title improperly sanitized. By creating a page with a specially crafted page title, a low privileged user can trigger arbitrary JavaScript execution.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
PiranhaCMS versions 7.0.0 to 9.1.1 are vulnerable to stored XSS via improperly sanitized page titles.
Vulnerability
PiranhaCMS versions 7.0.0 to 9.1.1 are vulnerable to stored cross-site scripting (XSS) due to improper sanitization of the page title. The title is rendered using v-html in the sitemap Vue component, allowing HTML injection. [1][2]
Exploitation
A low-privileged user with permission to create or edit pages can set a page title containing malicious HTML/JavaScript. When the sitemap is viewed by any user (including administrators), the injected script executes in the browser context of the viewing user. [1][2]
Impact
Successful exploitation allows arbitrary JavaScript execution within the context of the application, potentially leading to theft of sensitive information, session hijacking, or other malicious actions. The attack can affect any user who views the sitemap, including administrators. [1][2][4]
Mitigation
The vulnerability is fixed in version 9.2.0, released on October 25, 2021 [4]. Users should upgrade to version 9.2.0 or later. No workarounds are documented. [4]
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
PiranhaNuGet | >= 7.0.0, < 9.2.0 | 9.2.0 |
Affected products
2- PiranhaCMS/Piranhav5Range: 7.0.0
Patches
1543bc53c7dbdMake sure page title isn't rendered as HTML. Fixes #1726
3 files changed · +4 −4
core/Piranha.Manager/assets/dist/js/piranha.pagelist.js+1 −1 modified@@ -14,7 +14,7 @@ Vue.component("sitemap-item", { item.isExpanded = !item.isExpanded; } }, - template: "\n<li class=\"dd-item\" :class=\"{ expanded: item.isExpanded || item.items.length === 0 }\" :data-id=\"item.id\">\n <div class=\"sitemap-item\" :class=\"{ dimmed: item.isUnpublished || item.isScheduled }\">\n <div class=\"handle dd-handle\"><i class=\"fas fa-ellipsis-v\"></i></div>\n <div class=\"link\">\n <span class=\"actions\">\n <a v-if=\"item.items.length > 0 && item.isExpanded\" v-on:click.prevent=\"toggleItem(item)\" class=\"expand\" href=\"#\"><i class=\"fas fa-minus\"></i></a>\n <a v-if=\"item.items.length > 0 && !item.isExpanded\" v-on:click.prevent=\"toggleItem(item)\" class=\"expand\" href=\"#\"><i class=\"fas fa-plus\"></i></a>\n </span>\n <a v-if=\"piranha.permissions.pages.edit\" :href=\"piranha.baseUrl + item.editUrl + item.id\">\n <span v-html=\"item.title\"></span>\n <span v-if=\"item.isRestricted\" class=\"icon-restricted text-secondary small\"><i class=\"fas fa-lock\"></i></span>\n <span v-if=\"item.status\" class=\"badge badge-info\">{{ item.status }}</span>\n <span v-if=\"item.isScheduled\" class=\"badge badge-info\">{{ piranha.resources.texts.scheduled }}</span>\n <span v-if=\"item.isCopy\" class=\"badge badge-warning\">{{ piranha.resources.texts.copy }}</span>\n </a>\n <span v-else class=\"title\">\n <span v-html=\"item.title\"></span>\n <span v-if=\"item.isRestricted\" class=\"icon-restricted text-secondary small\"><i class=\"fas fa-lock\"></i></span>\n <span v-if=\"item.status\" class=\"badge badge-info\">{{ item.status }}</span>\n <span v-if=\"item.isScheduled\" class=\"badge badge-info\">{{ piranha.resources.texts.scheduled }}</span>\n <span v-if=\"item.isCopy\" class=\"badge badge-warning\">{{ piranha.resources.texts.copy }}</span>\n </span>\n </div>\n <div class=\"type d-none d-md-block\">{{ item.typeName }}</div>\n <div class=\"date d-none d-lg-block\">{{ item.published }}</div>\n <div class=\"actions\">\n <a v-if=\"piranha.permissions.pages.add\" href=\"#\" v-on:click.prevent=\"piranha.pagelist.add(item.siteId, item.id, true)\"><i class=\"fas fa-angle-down\"></i></a>\n <a v-if=\"piranha.permissions.pages.add\" href=\"#\" v-on:click.prevent=\"piranha.pagelist.add(item.siteId, item.id, false)\"><i class=\"fas fa-angle-right\"></i></a>\n <a v-if=\"piranha.permissions.pages.delete && item.items.length === 0\" v-on:click.prevent=\"piranha.pagelist.remove(item.id)\" class=\"danger\" href=\"#\"><i class=\"fas fa-trash\"></i></a>\n </div>\n </div>\n <ol v-if=\"item.items.length > 0\" class=\"dd-list\">\n <sitemap-item v-for=\"child in item.items\" v-bind:key=\"child.id\" v-bind:item=\"child\">\n </sitemap-item>\n </ol>\n</li>\n" + template: "\n<li class=\"dd-item\" :class=\"{ expanded: item.isExpanded || item.items.length === 0 }\" :data-id=\"item.id\">\n <div class=\"sitemap-item\" :class=\"{ dimmed: item.isUnpublished || item.isScheduled }\">\n <div class=\"handle dd-handle\"><i class=\"fas fa-ellipsis-v\"></i></div>\n <div class=\"link\">\n <span class=\"actions\">\n <a v-if=\"item.items.length > 0 && item.isExpanded\" v-on:click.prevent=\"toggleItem(item)\" class=\"expand\" href=\"#\"><i class=\"fas fa-minus\"></i></a>\n <a v-if=\"item.items.length > 0 && !item.isExpanded\" v-on:click.prevent=\"toggleItem(item)\" class=\"expand\" href=\"#\"><i class=\"fas fa-plus\"></i></a>\n </span>\n <a v-if=\"piranha.permissions.pages.edit\" :href=\"piranha.baseUrl + item.editUrl + item.id\">\n <span>{{ item.title }}</span>\n <span v-if=\"item.isRestricted\" class=\"icon-restricted text-secondary small\"><i class=\"fas fa-lock\"></i></span>\n <span v-if=\"item.status\" class=\"badge badge-info\">{{ item.status }}</span>\n <span v-if=\"item.isScheduled\" class=\"badge badge-info\">{{ piranha.resources.texts.scheduled }}</span>\n <span v-if=\"item.isCopy\" class=\"badge badge-warning\">{{ piranha.resources.texts.copy }}</span>\n </a>\n <span v-else class=\"title\">\n <span>{{ item.title }}</span>\n <span v-if=\"item.isRestricted\" class=\"icon-restricted text-secondary small\"><i class=\"fas fa-lock\"></i></span>\n <span v-if=\"item.status\" class=\"badge badge-info\">{{ item.status }}</span>\n <span v-if=\"item.isScheduled\" class=\"badge badge-info\">{{ piranha.resources.texts.scheduled }}</span>\n <span v-if=\"item.isCopy\" class=\"badge badge-warning\">{{ piranha.resources.texts.copy }}</span>\n </span>\n </div>\n <div class=\"type d-none d-md-block\">{{ item.typeName }}</div>\n <div class=\"date d-none d-lg-block\">{{ item.published }}</div>\n <div class=\"actions\">\n <a v-if=\"piranha.permissions.pages.add\" href=\"#\" v-on:click.prevent=\"piranha.pagelist.add(item.siteId, item.id, true)\"><i class=\"fas fa-angle-down\"></i></a>\n <a v-if=\"piranha.permissions.pages.add\" href=\"#\" v-on:click.prevent=\"piranha.pagelist.add(item.siteId, item.id, false)\"><i class=\"fas fa-angle-right\"></i></a>\n <a v-if=\"piranha.permissions.pages.delete && item.items.length === 0\" v-on:click.prevent=\"piranha.pagelist.remove(item.id)\" class=\"danger\" href=\"#\"><i class=\"fas fa-trash\"></i></a>\n </div>\n </div>\n <ol v-if=\"item.items.length > 0\" class=\"dd-list\">\n <sitemap-item v-for=\"child in item.items\" v-bind:key=\"child.id\" v-bind:item=\"child\">\n </sitemap-item>\n </ol>\n</li>\n" }); /*global piranha
core/Piranha.Manager/assets/dist/js/piranha.pagelist.min.js+1 −1 modified@@ -1 +1 @@ -Vue.component("pagecopy-item",{props:["item"],methods:{toggleItem:function(e){e.isExpanded=!e.isExpanded}},template:'\n<li class="dd-item" :class="{ expanded: item.isExpanded || item.items.length === 0 }">\n <div class="sitemap-item expanded">\n <div class="link" :class="{ readonly: item.isCopy }">\n <a v-if="!item.isCopy" :href="piranha.baseUrl + \'manager/page/copyrelative/\' + item.id + \'/\' + piranha.pagelist.addPageId + \'/\' + piranha.pagelist.addAfter">\n {{ item.title }}\n </a>\n <a href="#" v-else>\n {{ item.title }}\n <span v-if="item.isCopy" class="badge badge-warning">{{ piranha.resources.texts.copy }}</span>\n </a>\n <div class="content-blocker"></div>\n </div>\n <div class="type d-none d-md-block">\n {{ item.typeName }}\n </div>\n </div>\n <ol class="dd-list" v-if="item.items.length > 0">\n <pagecopy-item v-for="child in item.items" v-bind:key="child.id" v-bind:item="child"></pagecopy-item>\n </ol>\n</li>\n'}),Vue.component("sitemap-item",{props:["item"],methods:{toggleItem:function(e){e.isExpanded=!e.isExpanded}},template:'\n<li class="dd-item" :class="{ expanded: item.isExpanded || item.items.length === 0 }" :data-id="item.id">\n <div class="sitemap-item" :class="{ dimmed: item.isUnpublished || item.isScheduled }">\n <div class="handle dd-handle"><i class="fas fa-ellipsis-v"></i></div>\n <div class="link">\n <span class="actions">\n <a v-if="item.items.length > 0 && item.isExpanded" v-on:click.prevent="toggleItem(item)" class="expand" href="#"><i class="fas fa-minus"></i></a>\n <a v-if="item.items.length > 0 && !item.isExpanded" v-on:click.prevent="toggleItem(item)" class="expand" href="#"><i class="fas fa-plus"></i></a>\n </span>\n <a v-if="piranha.permissions.pages.edit" :href="piranha.baseUrl + item.editUrl + item.id">\n <span v-html="item.title"></span>\n <span v-if="item.isRestricted" class="icon-restricted text-secondary small"><i class="fas fa-lock"></i></span>\n <span v-if="item.status" class="badge badge-info">{{ item.status }}</span>\n <span v-if="item.isScheduled" class="badge badge-info">{{ piranha.resources.texts.scheduled }}</span>\n <span v-if="item.isCopy" class="badge badge-warning">{{ piranha.resources.texts.copy }}</span>\n </a>\n <span v-else class="title">\n <span v-html="item.title"></span>\n <span v-if="item.isRestricted" class="icon-restricted text-secondary small"><i class="fas fa-lock"></i></span>\n <span v-if="item.status" class="badge badge-info">{{ item.status }}</span>\n <span v-if="item.isScheduled" class="badge badge-info">{{ piranha.resources.texts.scheduled }}</span>\n <span v-if="item.isCopy" class="badge badge-warning">{{ piranha.resources.texts.copy }}</span>\n </span>\n </div>\n <div class="type d-none d-md-block">{{ item.typeName }}</div>\n <div class="date d-none d-lg-block">{{ item.published }}</div>\n <div class="actions">\n <a v-if="piranha.permissions.pages.add" href="#" v-on:click.prevent="piranha.pagelist.add(item.siteId, item.id, true)"><i class="fas fa-angle-down"></i></a>\n <a v-if="piranha.permissions.pages.add" href="#" v-on:click.prevent="piranha.pagelist.add(item.siteId, item.id, false)"><i class="fas fa-angle-right"></i></a>\n <a v-if="piranha.permissions.pages.delete && item.items.length === 0" v-on:click.prevent="piranha.pagelist.remove(item.id)" class="danger" href="#"><i class="fas fa-trash"></i></a>\n </div>\n </div>\n <ol v-if="item.items.length > 0" class="dd-list">\n <sitemap-item v-for="child in item.items" v-bind:key="child.id" v-bind:item="child">\n </sitemap-item>\n </ol>\n</li>\n'}),piranha.pagelist=new Vue({el:"#pagelist",data:{loading:!0,updateBindings:!1,items:[],sites:[],pageTypes:[],addSiteId:null,addSiteTitle:null,addPageId:null,addAfter:!0},methods:{load:function(){var e=this;piranha.permissions.load(function(){fetch(piranha.baseUrl+"manager/api/page/list").then(function(e){return e.json()}).then(function(i){e.sites=i.sites,e.pageTypes=i.pageTypes,e.updateBindings=!0}).catch(function(e){console.log("error:",e)})})},remove:function(e){var i=this;piranha.alert.open({title:piranha.resources.texts.delete,body:piranha.resources.texts.deletePageConfirm,confirmCss:"btn-danger",confirmIcon:"fas fa-trash",confirmText:piranha.resources.texts.delete,onConfirm:function(){fetch(piranha.baseUrl+"manager/api/page/delete/"+e).then(function(e){return e.json()}).then(function(e){piranha.notifications.push(e),i.load()}).catch(function(e){console.log("error:",e)})}})},bind:function(){var e=this;$(".sitemap-container").each(function(i,s){$(s).nestable({maxDepth:100,group:i,callback:function(i,s){fetch(piranha.baseUrl+"manager/api/page/move",{method:"post",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:$(s).attr("data-id"),items:$(i).nestable("serialize")})}).then(function(e){return e.json()}).then(function(i){piranha.notifications.push(i.status),"success"===i.status.type&&($(".sitemap-container").nestable("destroy"),e.sites=[],Vue.nextTick(function(){e.sites=i.sites,Vue.nextTick(function(){e.bind()})}))}).catch(function(e){console.log("error:",e)})}})})},add:function(e,i,s){var a=this;a.addSiteId=e,a.addPageId=i,a.addAfter=s,a.sites.forEach(function(i){i.id===e&&(a.addSiteTitle=i.title)}),$("#pageAddModal").modal("show")},selectSite:function(e){var i=this;i.addSiteId=e,i.sites.forEach(function(s){s.id===e&&(i.addSiteTitle=s.title)})},collapse:function(){for(var e=0;e<this.sites.length;e++)for(var i=0;i<this.sites[e].pages.length;i++)this.changeVisibility(this.sites[e].pages[i],!1)},expand:function(){for(var e=0;e<this.sites.length;e++)for(var i=0;i<this.sites[e].pages.length;i++)this.changeVisibility(this.sites[e].pages[i],!0)},changeVisibility:function(e,i){e.isExpanded=i;for(var s=0;s<e.items.length;s++)this.changeVisibility(e.items[s],i)}},created:function(){},updated:function(){this.updateBindings&&(this.bind(),this.updateBindings=!1),this.loading=!1}}); \ No newline at end of file +Vue.component("pagecopy-item",{props:["item"],methods:{toggleItem:function(e){e.isExpanded=!e.isExpanded}},template:'\n<li class="dd-item" :class="{ expanded: item.isExpanded || item.items.length === 0 }">\n <div class="sitemap-item expanded">\n <div class="link" :class="{ readonly: item.isCopy }">\n <a v-if="!item.isCopy" :href="piranha.baseUrl + \'manager/page/copyrelative/\' + item.id + \'/\' + piranha.pagelist.addPageId + \'/\' + piranha.pagelist.addAfter">\n {{ item.title }}\n </a>\n <a href="#" v-else>\n {{ item.title }}\n <span v-if="item.isCopy" class="badge badge-warning">{{ piranha.resources.texts.copy }}</span>\n </a>\n <div class="content-blocker"></div>\n </div>\n <div class="type d-none d-md-block">\n {{ item.typeName }}\n </div>\n </div>\n <ol class="dd-list" v-if="item.items.length > 0">\n <pagecopy-item v-for="child in item.items" v-bind:key="child.id" v-bind:item="child"></pagecopy-item>\n </ol>\n</li>\n'}),Vue.component("sitemap-item",{props:["item"],methods:{toggleItem:function(e){e.isExpanded=!e.isExpanded}},template:'\n<li class="dd-item" :class="{ expanded: item.isExpanded || item.items.length === 0 }" :data-id="item.id">\n <div class="sitemap-item" :class="{ dimmed: item.isUnpublished || item.isScheduled }">\n <div class="handle dd-handle"><i class="fas fa-ellipsis-v"></i></div>\n <div class="link">\n <span class="actions">\n <a v-if="item.items.length > 0 && item.isExpanded" v-on:click.prevent="toggleItem(item)" class="expand" href="#"><i class="fas fa-minus"></i></a>\n <a v-if="item.items.length > 0 && !item.isExpanded" v-on:click.prevent="toggleItem(item)" class="expand" href="#"><i class="fas fa-plus"></i></a>\n </span>\n <a v-if="piranha.permissions.pages.edit" :href="piranha.baseUrl + item.editUrl + item.id">\n <span>{{ item.title }}</span>\n <span v-if="item.isRestricted" class="icon-restricted text-secondary small"><i class="fas fa-lock"></i></span>\n <span v-if="item.status" class="badge badge-info">{{ item.status }}</span>\n <span v-if="item.isScheduled" class="badge badge-info">{{ piranha.resources.texts.scheduled }}</span>\n <span v-if="item.isCopy" class="badge badge-warning">{{ piranha.resources.texts.copy }}</span>\n </a>\n <span v-else class="title">\n <span>{{ item.title }}</span>\n <span v-if="item.isRestricted" class="icon-restricted text-secondary small"><i class="fas fa-lock"></i></span>\n <span v-if="item.status" class="badge badge-info">{{ item.status }}</span>\n <span v-if="item.isScheduled" class="badge badge-info">{{ piranha.resources.texts.scheduled }}</span>\n <span v-if="item.isCopy" class="badge badge-warning">{{ piranha.resources.texts.copy }}</span>\n </span>\n </div>\n <div class="type d-none d-md-block">{{ item.typeName }}</div>\n <div class="date d-none d-lg-block">{{ item.published }}</div>\n <div class="actions">\n <a v-if="piranha.permissions.pages.add" href="#" v-on:click.prevent="piranha.pagelist.add(item.siteId, item.id, true)"><i class="fas fa-angle-down"></i></a>\n <a v-if="piranha.permissions.pages.add" href="#" v-on:click.prevent="piranha.pagelist.add(item.siteId, item.id, false)"><i class="fas fa-angle-right"></i></a>\n <a v-if="piranha.permissions.pages.delete && item.items.length === 0" v-on:click.prevent="piranha.pagelist.remove(item.id)" class="danger" href="#"><i class="fas fa-trash"></i></a>\n </div>\n </div>\n <ol v-if="item.items.length > 0" class="dd-list">\n <sitemap-item v-for="child in item.items" v-bind:key="child.id" v-bind:item="child">\n </sitemap-item>\n </ol>\n</li>\n'}),piranha.pagelist=new Vue({el:"#pagelist",data:{loading:!0,updateBindings:!1,items:[],sites:[],pageTypes:[],addSiteId:null,addSiteTitle:null,addPageId:null,addAfter:!0},methods:{load:function(){var e=this;piranha.permissions.load(function(){fetch(piranha.baseUrl+"manager/api/page/list").then(function(e){return e.json()}).then(function(i){e.sites=i.sites,e.pageTypes=i.pageTypes,e.updateBindings=!0}).catch(function(e){console.log("error:",e)})})},remove:function(e){var i=this;piranha.alert.open({title:piranha.resources.texts.delete,body:piranha.resources.texts.deletePageConfirm,confirmCss:"btn-danger",confirmIcon:"fas fa-trash",confirmText:piranha.resources.texts.delete,onConfirm:function(){fetch(piranha.baseUrl+"manager/api/page/delete/"+e).then(function(e){return e.json()}).then(function(e){piranha.notifications.push(e),i.load()}).catch(function(e){console.log("error:",e)})}})},bind:function(){var e=this;$(".sitemap-container").each(function(i,s){$(s).nestable({maxDepth:100,group:i,callback:function(i,s){fetch(piranha.baseUrl+"manager/api/page/move",{method:"post",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:$(s).attr("data-id"),items:$(i).nestable("serialize")})}).then(function(e){return e.json()}).then(function(i){piranha.notifications.push(i.status),"success"===i.status.type&&($(".sitemap-container").nestable("destroy"),e.sites=[],Vue.nextTick(function(){e.sites=i.sites,Vue.nextTick(function(){e.bind()})}))}).catch(function(e){console.log("error:",e)})}})})},add:function(e,i,s){var a=this;a.addSiteId=e,a.addPageId=i,a.addAfter=s,a.sites.forEach(function(i){i.id===e&&(a.addSiteTitle=i.title)}),$("#pageAddModal").modal("show")},selectSite:function(e){var i=this;i.addSiteId=e,i.sites.forEach(function(s){s.id===e&&(i.addSiteTitle=s.title)})},collapse:function(){for(var e=0;e<this.sites.length;e++)for(var i=0;i<this.sites[e].pages.length;i++)this.changeVisibility(this.sites[e].pages[i],!1)},expand:function(){for(var e=0;e<this.sites.length;e++)for(var i=0;i<this.sites[e].pages.length;i++)this.changeVisibility(this.sites[e].pages[i],!0)},changeVisibility:function(e,i){e.isExpanded=i;for(var s=0;s<e.items.length;s++)this.changeVisibility(e.items[s],i)}},created:function(){},updated:function(){this.updateBindings&&(this.bind(),this.updateBindings=!1),this.loading=!1}}); \ No newline at end of file
core/Piranha.Manager/assets/src/js/components/sitemap-item.vue+2 −2 modified@@ -8,14 +8,14 @@ <a v-if="item.items.length > 0 && !item.isExpanded" v-on:click.prevent="toggleItem(item)" class="expand" href="#"><i class="fas fa-plus"></i></a> </span> <a v-if="piranha.permissions.pages.edit" :href="piranha.baseUrl + item.editUrl + item.id"> - <span v-html="item.title"></span> + <span>{{ item.title }}</span> <span v-if="item.isRestricted" class="icon-restricted text-secondary small"><i class="fas fa-lock"></i></span> <span v-if="item.status" class="badge badge-info">{{ item.status }}</span> <span v-if="item.isScheduled" class="badge badge-info">{{ piranha.resources.texts.scheduled }}</span> <span v-if="item.isCopy" class="badge badge-warning">{{ piranha.resources.texts.copy }}</span> </a> <span v-else class="title"> - <span v-html="item.title"></span> + <span>{{ item.title }}</span> <span v-if="item.isRestricted" class="icon-restricted text-secondary small"><i class="fas fa-lock"></i></span> <span v-if="item.status" class="badge badge-info">{{ item.status }}</span> <span v-if="item.isScheduled" class="badge badge-info">{{ piranha.resources.texts.scheduled }}</span>
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-jvjp-vh27-r9h5ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-25977ghsaADVISORY
- github.com/PiranhaCMS/piranha.core/commit/543bc53c7dbd28c793ec960b57fb0e716c6b18d7ghsax_refsource_MISCWEB
- www.whitesourcesoftware.com/vulnerability-database/CVE-2021-25977ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.