Client-Side Template Injection (CSTI)
debt(d7/e5/b5/t7)
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.
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.
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.
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.
Also Known As
TL;DR
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
Common Misconception
Why It Matters
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
<!-- ❌ 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>
<!-- ✅ 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>