An AngularJS expression evaluated in the browser is not reflected XSS and not SSTI - a separate class where the confirming artifact is an arithmetic oracle resolved by the template engine in the DOM.
Client-side template injection is the class where user input reaches a template expression context that is evaluated by a JavaScript framework in the browser - not by the server. It shares a confirmation mechanic with SSTI (an arithmetic oracle) and a browser runtime with DOM XSS, but it is neither. A scanner keyed on server error messages misses it entirely; one keyed only on DOM sinks misses the framework expression context. Probing it correctly means understanding which engine runs the template and what capability a confirmed evaluation enables.
In reflected and stored XSS the vector is a value the browser writes into the DOM or an event handler. In SSTI the vector reaches a server-side engine. In CSTI the vector reaches a client-side template compiler - AngularJS's $compile, Angular's JIT runtime, Vue's in-browser template compiler, or Handlebars rendered on the page. The browser runs the expression; the server is a bystander that handed the template to the page in the first place.
The confirming probe is identical to SSTI in its outward form but the execution context is the browser. Submit the expression {{7919*7907}} into a field whose value is interpolated by a framework template. If the DOM shows 62615533, the framework evaluated the expression - not just reflected it. That is in-browser proof of evaluation with no dangerous gadget, no real command, and no code run beyond an arithmetic multiply.
// AngularJS 1.x: inject into a scope-bound interpolation field // payload lands in a {{expr}} binding the $compile step evaluates const probe = "{{7919*7907}}"; // submit via the input that populates a binding, e.g. a search label: // <h1>Results for {{userQuery}}</h1> // confirming artifact: DOM text node reads 62615533, not the literal string const text = document.querySelector('h1').textContent; if (text.includes('62615533')) { // expression was evaluated by $compile - CSTI confirmed report('csti-confirmed', { payload: probe, dom: text }); } // a plain-string reflect (no evaluation): DOM shows {{7919*7907}} literally // HTTP response inspection alone cannot distinguish these two cases
The server response is identical in both cases - the literal string {{7919*7907}} travels the wire. The difference is what the framework does with it when it compiles the template. An HTTP-level scanner that reads the response body sees no signal either way; the confirming artifact is in the DOM after the page renders, which is why headless-browser evaluation is the correct tool for this class.
AngularJS (1.x) is the most commonly encountered CSTI surface: its $compile phase evaluates every {{expr}} in the DOM, and documented sandbox-escape chains (Gareth Heyes, 2016) turn expression evaluation into arbitrary JavaScript execution - which in a browser context means full DOM access and credential theft without any server-side component. Angular 2+ compiles templates ahead-of-time (AOT) by default, but JIT mode leaves the surface open. Vue's in-browser template compiler, used when runtime-compiler is loaded and user input reaches v-html or a directive that calls Vue.compile, shares the same class. Handlebars rendered in the browser with a triple-mustache ({{{unsafe}}}) that the application treats as a template is a fourth variant with its own confirmation path.
| Framework | Evaluation trigger | Confirming oracle | Max severity |
|---|---|---|---|
| AngularJS 1.x | Any {{expr}} in $compile scope; ng-app on ancestor element | {{7919*7907}} resolves to 62615533 in DOM text | Critical - sandbox escapes yield JS execution |
| Angular 2+ JIT | JIT mode only; template string passed to compiler at runtime | Same arithmetic oracle; scope differs | High - sandbox narrower than AngularJS 1.x |
| Vue (browser compiler) | v-html or dynamic template with runtime-compiler build | {{7919*7907}} in compiled binding | High - expression context limited |
| Handlebars (browser) | Triple-mustache {{{expr}}} or registerHelper with user input | Arithmetic in helper return value | Medium to high - depends on available helpers |
A probe keyed on AngularJS alone silently misses three other frameworks. Detecting the loaded engine first - by fingerprinting angular, Vue, or Handlebars on window before sending the arithmetic probe - avoids the false negative a single-framework check creates.
An arithmetic oracle that resolves to 62615533 is confirmed evaluation - rated high. A sandbox escape that lets the expression call constructor to reach a real function and execute arbitrary JavaScript is rated critical, because DOM access, session theft and exfiltration are all in scope once arbitrary code runs in the browser page. The expression that merely evaluates a number is not the severity anchor - the capability the engine exposes to that expression is.
The inverse also applies: a Vue v-html binding that reflects user HTML verbatim is a stored or reflected XSS finding (DOM write), not a CSTI finding, unless the runtime compiler actually evaluates the injected string as a template. Conflating the two inflates CSTI counts and mislabels the fix: the XSS path is closed by sanitizing the value before the DOM write; the CSTI path is closed by never passing user input through a template compilation step.
The fix is on the application side: never pass untrusted input into a $compile call, a JIT template string, a Vue.compile result, or a triple-mustache binding. Render dynamic content as data, not as a template - the one-way gate is the framework's own server-side or build-time rendering path, where user values are substituted as strings, not evaluated as expressions.