VYPR
Moderate severityNVD Advisory· Published Oct 25, 2021· Updated Apr 30, 2025

Piranha CMS - Stored XSS in Page Title

CVE-2021-25977

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.

PackageAffected versionsPatched versions
PiranhaNuGet
>= 7.0.0, < 9.2.09.2.0

Affected products

2

Patches

1
543bc53c7dbd

Make sure page title isn't rendered as HTML. Fixes #1726

https://github.com/PiranhaCMS/piranha.coreHåkan EdlingSep 26, 2021via ghsa
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

News mentions

0

No linked articles in our index yet.