Reflected XSS AngularJS sandbox escape CSP
| Field | Value |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Difficulty | Expert |
| Vulnerability | AngularJS Sandbox Escape + CSP Bypass |
| Injection Point | search URL parameter → AngularJS ng-focus directive |
| Goal | Execute alert(document.cookie) bypassing both the AngularJS sandbox and CSP |
Lab — AngularJS Sandbox Escape + CSP Bypass¶
What is CSP and Why Does it Matter Here?¶
Content Security Policy (CSP) is an HTTP response header that tells the browser which resources it is allowed to load and execute. It is a defence-in-depth layer on top of other protections.
Solution Walkthrough¶
Step 1 — Confirm what the CSP blocks
HTML injection works:
<marquee>teto</marquee>
But inline scripts are blocked by CSP:
<script>alert(0)</script>
The browser blocks the script because script-src 'self' does not permit inline scripts. The <script> tag appears in the HTML but never executes.
The CSP for this lab (visible in the Network tab):
content-security-policy:
default-src 'self';
script-src 'self';
style-src 'unsafe-inline' 'self'
Breaking it down:
default-src 'self'— only load resources from the same originscript-src 'self'— only execute JavaScript from the same origin; inline<script>tags are blocked,eval()is blockedstyle-src 'unsafe-inline' 'self'— inline styles are allowed (this is a hint)
Step 2 — Find the CSP bypass via AngularJS
Since style-src 'unsafe-inline' is permitted and AngularJS is loaded on the page, we need a payload that:
- Does not use a
<script>tag (blocked by CSP) - Uses AngularJS directives to execute JavaScript (Angular is already trusted)
From the PortSwigger XSS cheat sheet — AngularJS CSP bypass:
<input id=x ng-focus=$event.composedPath()|orderBy:'(z=alert)(1)'>
Posting this in the search bar and clicking (focusing) the input:
Alert fires. The CSP is bypassed.
How the Payload Works¶
Let's break down every piece:
<input id=x ng-focus=...>
An <input> element with an AngularJS ng-focus directive. ng-focus tells AngularJS to evaluate the given expression when the element receives focus. This is not a <script> tag — CSP allows it because it's an HTML attribute being processed by AngularJS, which is itself a trusted same-origin script.
[!note] This is the CSP bypass mechanism.
script-src 'self'blocks inline JavaScript but does not block Angular directives embedded in HTML attributes. AngularJS is loaded from the same origin — it is trusted. When Angular evaluatesng-focus, it runs inside the trusted Angular context, not as a new inline script.
$event.composedPath()
$event is the Angular representation of the DOM event (the focus event in this case). .composedPath() returns an array of DOM elements involved in the event path — from the target element up to the window.
The reason this is needed: orderBy requires an iterable input to sort. $event.composedPath() provides that array, giving orderBy something to work with. Without it, orderBy has nothing to operate on.
|orderBy:'(z=alert)(1)'
orderBy is an Angular filter that sorts an array. Its sort key argument is evaluated as an Angular expression. This is the code execution vehicle — orderBy evaluates expressions, and since we already have the Angular context, our expression executes.
(z=alert)(1) — this is the sandbox bypass for the string restriction:
z=alertassigns thealertfunction to variablez(z=alert)evaluates to thealertfunction itself(z=alert)(1)immediately calls that function with argument1
This is equivalent to alert(1) but avoids writing it directly — wrapping in an assignment expression makes Angular evaluate it as a value assignment rather than flagging it as a direct function call.
Step 3 — Deliver to the victim and steal document.cookie
The payload needs to:
1. Navigate the victim to the vulnerable page with the injected input
2. Auto-focus the input (using the #x URL fragment — the browser focuses the element with that ID)
3. Execute alert(document.cookie) instead of alert(1)
Exploit server payload:
<script>
location = 'https://TARGET.web-security-academy.net/?search=<input id=x+ng-focus=$event.composedPath()|orderBy:%27(z=alert)(document.cookie)%27>#x';
</script>
Why URL encoding:
- %27 = ' (single quote — needed for the orderBy string argument)
- + in id=x+ng-focus = space character in URL encoding (separates id=x from ng-focus)
- #x — the URL fragment that auto-focuses the element with id="x", triggering ng-focus without any user interaction
Lab solved :P
Why This Bypasses CSP¶
The CSP blocks:
- <script>alert(1)</script> ❌ — inline script, blocked by script-src 'self'
- eval('alert(1)') ❌ — eval blocked by absence of 'unsafe-eval'
The CSP does not block:
- AngularJS itself — it is loaded from the same origin ('self')
- HTML attributes containing Angular directives — not JavaScript, just HTML
- Angular's internal expression evaluation — Angular is trusted; what Angular does internally is not subject to CSP
Analogy: CSP is like a bouncer who checks ID at the door. AngularJS has already been let in (it's from the same origin). Once inside, AngularJS evaluates expressions from HTML attributes — the bouncer never checks what AngularJS does after entering because it was already verified at the door.