When Vue Forgets to Escape: Multiple Stored XSS Vectors Caused by Missing v-pre directive
TL;DR
Statamic’s Control Panel uses Vue.js to render many admin views. Vue compiles and evaluates mustache expressions like {{ ... }} in the browser - so {{7*7}} becomes 49 in the client-side DOM while the server still stores the literal {{7*7}}.
Because of this, unescaped user input inside {{ }} can be treated as executable template code in the browser when the component isn’t protected with v-pre.
My research proved, that it is possible to trigger an xss in such an environment, by simply using the following payload:
{{this.constructor.constructor('alert(document.cookie)')()}}
When injected and saved into an input field, that string is compiled and executed in the browser.
Local setup
- Statamic CMS: 4.42.0
- Framework: Laravel (via Laravel Sail)
- Runtime: Docker Desktop on macOS (local instance)
- DB: MySQL
Affected endpoints
The following Control Panel sections were vulnerable to stored XSS (<=4.42.0):
- /collections
- /navigation
- /taxonomies
- /asset-containers
- /globals
- /forms
Refusing to Let It Slide: Escalation and Takeover
Even though it was a simple XSS, without losing momentum, I decided to push further and transform this basic vulnerability into something far more impactful. Through methodical chaining of weaknesses, I was able to escalate privileges and achieve a complete Superadmin account takeover - the next blogpost documents the exploitation steps and required conditions. ---> https://blog.wojtekchwala.workers.dev/blog/when-vue-forgets-from-a-missing-v-pre-to-superadmin-takeover/
References
https://github.com/statamic/cms/pull/9256
https://github.com/statamic/cms/commit/990a748335f45abd848eed6f0250d7278f30244e