Post

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

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

Lab

PortSwigger Web Security Academy — DOM XSS in AngularJS expression with angle brackets and double quotes HTML-encoded

Difficulty: Practitioner
CWE: CWE-79 (Cross-Site Scripting)
Sub-class: Client-Side Template Injection (CSTI) via AngularJS


The Vulnerability

The application uses AngularJS (ng-app directive) and reflects the ?search= parameter inside the Angular template scope. The server HTML-encodes <, >, and " — blocking standard tag injection. But AngularJS evaluates `` template expressions client-side, entirely independently of HTML encoding.

An attacker can inject a valid AngularJS expression that calls arbitrary JavaScript without using a single angle bracket.


Fingerprinting

Inject `` in the search box. If the page renders 49, the input is evaluated as an AngularJS expression and the page is exploitable.


The Payload

1
PartRole
$onAlways-present AngularJS scope method
.constructorJavaScript Function constructor
('alert(1)')Creates a Function with body alert(1)
()Immediately invokes it

No <, >, or " required — the filter is irrelevant.


Working Request

1
2
GET /?search=%7B%7B%24on.constructor%28%27alert%281%29%27%29%28%29%7D%7D HTTP/2
Host: <lab-id>.web-security-academy.net

AngularJS evaluates the expression client-side → alert(1) fires → lab solved.


Shell-Quoting Gotcha

$on is a shell variable reference. Passing the payload naively to bash expands it to empty string (`` — invalid, no alert). Build the payload safely in Python:

1
payload = ' + chr(36) + "on.constructor('

Why HTML Encoding Doesn’t Help

1
2
Filter blocks:   <script>alert(1)</script>   →   &lt;script&gt;...
Attacker sends:    →   no < or > needed

The server defends against HTML tag injection but AngularJS creates a second execution boundary that runs after the HTML is parsed — completely outside the scope of HTML encoding.


Fix

  • Do not reflect untrusted input into an AngularJS `` expression context.
  • Use ng-bind (text-only interpolation) instead of `` for user-supplied content.
  • Migrate from AngularJS 1.x (reached EOL 2021) to Angular 2+ which removes this vector.
  • Apply a Content Security Policy (script-src 'self') to limit script sources.

CWE-79: https://cwe.mitre.org/data/definitions/79.html

This post is licensed under CC BY 4.0 by the author.