No matter what one thinks, it seems that the JS-heavy single page application paradigm is here to stay. Front-end developers rave about React and coo at Vue, keeping the user experience smooth and uninterrupted as fetch, XHR and service workers do all the dirty work interfacing with backend API services behind the scenes. SPA detractors talk about extreme code bloat, the insane size and shape of node_modules directories, and how all of this JS-heavy front-end code makes auditing the applications somewhat harder to understand and debug.
Complexity begets complexity
A common web application frontend these days is written without any
.js files at all — in the case of TypeScript+React, our folder structures are all
Source maps are information disclosure
If we agree to the concept that the Webpacked bundle is the intermediate representation of the application code, we can make the analogy that the bundle is the closest thing to a binary that we commonly ship to a browser. Just as we can decompile a binary and read the assembly code in IDA Pro or another disassembler, we can do the same to the application bundle; it just requires more skill and time to figure out what is going on in the packed/transpiled variant than it does to start if we started with the source code in the first place. Holding these assumptions true, source maps for transpiled SPAs can logically be treated as a source code leak (CWE 540: Information Exposure through Source Code).
For an application security engineer, source maps contain:
- Information about the developer’s source code paths. The
contentsfield of the source map from Webpack contain the relative paths used in the build process. This is information that can be fed back into systems where we may be able to exfiltrate source code by having a better idea of what the paths are where the source code lies, such as in local file inclusion vulnerabilities.
- Developer comments and doc strings. Commented-out functions, developer names, email addresses, documentation strings, and sometimes even expected API server responses exist in the uncompressed source code; this is stripped out in the default Webpack minification process.
- Your web application’s raw front end source. This code contains all original function names, variable names, and strings, in their original filenames and languages. This makes the code significantly easier to read and turns reverse engineering into an easy code review, where the developers are guiding you through the process of understanding the problems in their codebase. Go ahead and easily dig around for template injection or dangerouslySetInnerHTML and have a better sense of what’s going on. The source map is effectively giving away, in documented and organized form, the entire front-end half of your application to everyone who visits your production website, trivially forked and hacked on by competitors or anyone in jurisdictions where intellectual property protection is lax.
Note that some popular web developers such as Chris Coyier did not seem to care about this information disclosure. After all, what’s on the client is on the client anyway in minified/compressed form, so why does it matter? Stated in the above article:
The benefits [of source maps in production] boil down to these two things:
1. It might help you track down bugs in production more easily
2. It helps other people learn from your website more easily
The third rule ignored above is that production source maps also help malicious users track down those same bugs in production. Furthermore, it is tough to explain why this is the case to the non-technical. Your company’s executives, middle managers, and legal teams would want to know why the “proprietary and confidential” TypeScript/React source code is sitting somewhere on a public web server comments and all. I do not know of many corporate clients that I could convince of this being a good idea for a host of reasons.
Remember that application security is about raising the cost for an adversary, and mitigating business risk therein. Publishing production source maps to the public solve neither problem; instead, it exacerbates both problems. All of the information listed above is useful on a security assessment and is thus useful to penetration testers outside of your organization as well.
If only there was an easy way to get this into a manageable form…
After having run into these source maps a few times, both on bug bounties and actual engagements, I wrote a tool that allows me to quickly grab them from Webpack-generated bundles and sources. Alongside this post I have open-sourced the tool under the MIT license for other appsec engineers to use on their own assessments. It is now available on GitHub.
Un-Webpacking a victim application
In order to understand the impact of source maps, an example TypeScript+React application is shipped along side the script at
example-react-ts-app/. For this application, I simply took a random TypeScript+Webpack+React boilerplate from GitHub and made some quick changes to demonstrate the potential impact.
To run the application, simply
yarn install inside the directory and then run
npm run start-prod to run the application on port 3000. This is what our very ugly application looks like in Chromium on Linux
If we look at this in the browser, there is not much to see. Many names have been mangled by the process and we have lost much sense of the SPA’s context:
Usually, security engineers and bug hunters will use a tool such as js-beautify to get a better sense of what all these things are and what is going on. However, why not just use the source map?
./unwebpack_sourcemap.py --detect "http://localhost:3000/" output
Now, in the output directory, I have a file structure similar to what the developers would see. We can open it in Visual Studio Code and recover the actual TSX file for the application from the source map.
Also, note that we have a reference to fakeLibrary, something in lib/LibraryCode.ts. This is easy for us to find as we have a semi-working directory structure reversed from the source map content. What’s in there?
We have the code as it exists to the developers, with the maintainers, comments and all. This makes it more obvious that in this example, the
HEADER constant is used as a hard-coded bearer token. Also, because we have comments, we also have dead code:
This means your black-box test is actually a little closer to a gray-box test if you can find a source map. You now are extremely close to the representation of the SPA you would get in a code drop from a client or development team.
A good security post isn’t without some remediation advice — and if you have better advice, please share it in comments below. While Webpack’s documentation talks about some best practices for production source maps, there are a couple ways of dealing with this risk:
- Don’t turn on source maps in production. This may frustrate debugging but the fastest way to mitigate the risk is eliminate it.
- Put source maps on a private server, accessible internally to your dev team. Source maps do not need to be published alongside the code; they can be put on a different server that is appropriately inaccessible to others.
- ACL the sourcemaps in your existing server configuration. I don’t really like this idea, but some people suggested it. If you don’t have to publish raw source to a production server, don’t do it. Web server misconfiguration has historically been a problem for server-side source code leaks.
- Use a more restrictive source map. This mitigates some source leakage but not the entirety of the disclosure. Rails’s webpacker moved to nosources-source-map as the production default.
If you are a web developer, hopefully you will take some of this under advisement and restrict your source maps in production. Note that no matter what you do, client code is client code, and a determined adversary is going to reverse it. What you do by restricting source maps is a) raise the effort level required to dig through your SPA, and b) avoid unintentional leakage of developer names, usernames, comments, and keys that you may think aren’t making it into your “compiled” application.
If you would like to offer better constructive advice for this article, please leave a comment. I am always happy to see feedback from others that can help better secure web development practices.