Skip to main content

Command Palette

Search for a command to run...

PortSwigger XSS Lab: DOM XSS in AngularJS

DOM XSS in AngularJS expression with angle brackets and double quotes HTML-encoded

Updated
5 min read
PortSwigger XSS Lab: DOM XSS in AngularJS

Description

This lab contains a DOM-based cross-site scripting vulnerability in a AngularJS expression within the search functionality.

AngularJS is a popular JavaScript library, which scans the contents of HTML nodes containing the ng-app attribute (also known as an AngularJS directive). When a directive is added to the HTML code, you can execute JavaScript expressions within double curly braces. This technique is useful when angle brackets are being encoded.

Task

To solve this lab, perform a cross-site scripting attack that executes an AngularJS expression and calls the alert function.

Methodology

  • Add the Target URL in Burpsuite Scope

  • Lets identify the framework and its version for the current website

  • As seen in the above image, the website is running AngularJS 1.7.7

  • AngularJS below 1.7.8 and above 1+ use the $scope method which is used to bind data between controller and view (DOM)

  • It plays a key role in the two-way data binding mechanism.

  • When a controller sets values on $scope, all DOM elements that use that controller automatically get access to those properties and methods.

  • $scope follows a prototypal inheritance model.

Note: AngularJS evaluates expressions in the context of $rootScope if no controller binding exists.

  • Any property or methods defined in the controller is accessible to the DOM elements inside that controller’s scope.

  • The catch is

    Even if no properties are explicitly bound in the DOM, the DOM nodes (via AngularJS directives like ng-controller, ng-repeat, etc.) are still under the influence of the $scope object from the controller.

  • So, even if a DOM node doesn’t use any {{ expression }} or directive, the scope still applies and exists for it — it's just not visible until you tap into it.

  • Because of JavaScript’s prototypal inheritance, any HTML node under an AngularJS controller:

    • Gets associated with a scope object, and

    • That scope object inherits from $rootScope, which provides built-in methods like:

      • $eval()

      • $on()

      • $watch()

      • $emit()

      • $broadcast()

  • Even if the DOM element doesn't use {{ }} or bind any model, as long as it's within the controller's scope, it inherits those scope methods through the prototype chain.

  • Based on this logic, let us construct our payload

      {{ $on.constructor('alert(1)')() }}
    
    • $on is a function (defined on the prototype of $rootScope).

    • In JavaScript, all functions are objects, and all function objects have a .constructor property.

    • So when you do:

        $scope.$on.constructor
      
    • You're accessing the .constructor property of the $on function object

    • This returns the native Function constructor:

        console.log($scope.$on.constructor === Function); // ✅ true
      
    • $on.constructor('alert(1)') — Evaluates to a function equivalent to new Function('alert(1)')

    • (): Immediately invokes that function

    • Because, In JavaScript, functions are first-class objects, and you can:

      • Create a function dynamically (e.g., via Function constructor)

      • Call it immediately using () — even in the same line

  • Let’s execute the payload in the search bar of the website

  • After successful execution of the JS exploit, the lab will be solved

  • Using the above payload, we were able to leverage prototypal inheritance in JavaScript to access the $on method, retrieve its .constructor (which points to the native Function constructor), and dynamically create a new global function.

  • By appending () at the end, we immediately invoked the generated function, which executed the alert() call — resulting in an alert popup, all within a single line of code.

  • In summary, AngularJS expressions can be abused when user-controlled data is processed by the Angular template engine. By leveraging prototype inheritance and accessing the Function constructor through $on.constructor(), an attacker can escape AngularJS sandboxing and execute arbitrary JavaScript.

Remediation

  • Use AngularJS 1.8.0+ patched sandbox

  • Disable expression evaluation (strictContextualEscaping)

  • Use Content Security Policy (CSP)

  • Sanitize user input before rendering

Sandboxing in AngularJS

Why {{ }} Is Used in AngularJS

AngularJS uses double curly braces ({{ }}) for expression binding, also known as interpolation.

This syntax allows dynamic values to be inserted into the DOM based on JavaScript expressions evaluated by AngularJS. It enables the view (HTML) to reactively display data managed by the controller.

Example:

<p>Hello {{ username }}!</p>

If $scope.username = "Logan" in the controller, AngularJS replaces the placeholder dynamically with:

Hello Logan!

It can also evaluate JavaScript expressions:

{{ 2 + 2 }}           → 4
{{ username.toUpperCase() }} → LOGAN

Key points:

  • {{ }} acts like a safe, mini expression parser.

  • It updates automatically when scope values change (two-way binding).

  • It avoids writing full <script> tags inside HTML.

This mechanism improves templating — but also becomes dangerous when user input is parsed as an expression, leading to AngularJS-based XSS.

What Is AngularJS Sandboxing?

AngularJS includes a sandbox, which is a restricted execution environment designed to prevent template expressions from running arbitrary JavaScript.

In theory, this means expressions inside {{ }} should only access a limited safe subset of functions - NOT the entire DOM or global JavaScript environment.

For example, the sandbox is meant to allow:

{{ 1 + 1 }}  ✔
{{ user.name }} ✔

But block:

{{ alert(1) }} ❌
{{ constructor.constructor('alert(1)')() }} ❌

However — earlier AngularJS versions (including 1.7.7, as in this lab) had sandbox escape vulnerabilities. By abusing prototype inheritance and accessing internal function constructors, attackers can bypass the sandbox and execute real JavaScript.

So the payload:

{{ $on.constructor('alert(1)')() }}

works because it breaks out of the sandbox and executes code in the page’s JavaScript context -effectively turning a harmless template expression into a stored or reflected XSS.

CSTI: The Origin of This Vulnerability Class

This behavior led to the emergence of a new type of security issue known as Client-Side Template Injection (CSTI).

In AngularJS and other JavaScript frameworks that support client-side templating, user-controlled input may be interpreted as executable template code rather than plain text. When untrusted data reaches the Angular interpolation engine ({{ }}), attackers can inject expressions and begin interacting with the framework:

{{ 7*7 }} → CSTI detection

From there, chaining prototype abuse with sandbox bypass techniques allows escalation to full script execution:

{{ $on.constructor('alert(1)')() }} → XSS
126 views