The past decade witnessed a meteoric rise in the number of applications adopting the Single-Page Application (SPA) model. Such applications are designed in such a way that the content on every new page within the application appears on a single web page, without having to load new HTML pages. Single-Page Applications leverage JavaScript capabilities to manipulate Document Object Model (DOM) elements, allowing to update all the content on the same page.
A more traditional web page architecture loads different pages as the user attempts to open a new web page. In this case, each HTML page is usually linked to other pages. Upon each page load, the browser fetches and displays a fresh page.
Whereas, Single-Page Applications are enabled by JavaScript frameworks such as React, Angular, and VueJS. These frameworks help to conceal the complex functions that SPAs perform. They also provide additional benefits such as modular reusable components, state management, etc. Such modern frameworks help SPAs execute web pages effortlessly, as compared to multi-page applications that use Vanilla JavaScript. Using plain JavaScript makes it difficult to keep the UI updated with dynamic state changes.
The emergence of such frameworks causes changes in the security implications it may have on the frontend. Therefore, it is necessary to understand their internal machinery while disassembling client-side code and how modern frameworks change the attack vectors.
Build Process
As is customary, a developer creates a web page by defining the page structure in an HTML file and its styling in a CSS file. Then, it is linked in HTML using tags such as <style> or <link>, by embedding JavaScript code with <script> tags. However, building Single-Page Applications is more complicated than this.
Frameworks such as React and Vuejs provide a virtual DOM, which allows you to steer clear of raw HTML. A virtual DOM, unlike a normal DOM, is a concept in which the representation of the UI is stored in memory, instead of rendering it on the browser with every change. This allows faster changes to DOM. The code is also written in modular JavaScript files which is then processed in the build process.
Here’s a diagram overviewing the build process:
Transpiling
JavaScript is a dialect of ECMAScript and it isn’t a fixed standard; new features are added to ECMAScript with newer standards, every few years. While newer standards are released frequently, no browser JavaScript engine (Chromium V8, Safari Javascript Core, Firefox SpiderMonkey) fully implements all the ECMA specifications, each having certain differences in features they support. Also your code needs to be compatible with older browsers released to support older specifications alone.
Transpiling enables you to write modern code which is then converted to standards and features that are supported everywhere, for e.g. ES6 -> ES5. You might use const, let to define your variables but the transpiler helps to convert it to var, internally.
Transpiling is also used to convert code in JavaScript dialects such as Typescript and Coffeescript to plain JavaScript. Each dialect contains its own transpiler such as tsc for Typescript. Babel is the most prevalent transpiler.
Bundling
A typical modern app contains hundreds of external dependencies when you look at it recursively and it isn’t practical to load each of those separately in a script tag.
Through the process of bundling, you can take each import or require statement, find the source files and bundle them all into one single JavaScript file, applying appropriate scoping.
With the help of this process, all of the code that makes the application work is bundled together; the business logic code as well as the boiler-plate code are bundled together.
Minification/ Obfuscation
The final JavaScript output usually can be very large due to extra spaces or unnecessary, redundant data. Therefore, the final step in the build process is to minify the code. During the process of minification, comments/ whitespaces are removed, identifiers are renamed to shorter variants, and certain routines are simplified. This process then leads to obfuscation, wherein the final code is unreadable and differs highly from the source code.
Bundling and minification is usually done using Webpack.
Reverse engineering code
Once the code has been minified in the build process, security researchers will have to de-minify it to study the app. As most of the information such as variable names are lost during the process of minification, there isn’t a straightforward way to do this. However, there are certain tools to aid you in the process such as jsnice.org.
This tool uses machine learning to restore minified variable, function names and infer type information. It also formats the code and adds comments.
After this step, you are still left with a bundled code, but the main logic would be readable.
To debundle it into modules, we need to know how Webpack or any other bundler works.
Debundling
A bundler starts with an entry point file – the root of your application logic – and traces import and require statements. It then builds a dependency graph, module A requires B which requires C and D, and so on.
If you look through the Webpack chunks after they have been passed through jsnice, you’ll find a lot of calls to “__webpack_require__(<number>).”
“ __webpack_require__” is similar to the require JavaScript syntax in functionality, except it contains the logic to load modules from the chunks.
The only way to unbundle a bundle is to construct the abstract syntax tree (AST) manually, as there have been attempts to debundle that aren’t maintained anymore.
You could use these resources to study in depth how a bundle file works and to know the internals of Webpack. In this video, Webpack founder Tobias Koppers shows us how bundling is done manually.
Security Tip
How do these frameworks change attack vectors?
Reduced XSS
React does not render dynamic HTML; it sanitizes content coming from all variables, even if they do not contain dynamic content. Here XSS is all but eradicated unless the developer uses an unsafe function such as dangerouslySetInnerHTML.
In that case, even if you find data reflection you would not be able to insert HTML.
CSP-Bypasses
You could use certain gadgets available in Angular to bypass Content Security Policy (CSP) in certain cases.
<img src=”/” ng-on-error=”$event.srcElement.ownerDocument.defaultView.alert($event.srcElement.ownerDocument.domain)”
/>
Here, if CSP is blocking inline scripts but the page is using Angular, you could use the ng-on-error attribute to get Angular to execute the JavaScript. These types of gadgets are often patched but discovered regularly in Vuejs and Angularjs.