Extracting JavaScript from Sourcemaps

Sep 7 2018

Sourcemaps are intended to make debugging minified JavaScript less of a pain. This article will take a closer look at Sourcemaps and discuss retrieving the original source code tree from a downloaded Sourcemap file.

I recently reviewed a ReactJS site that communicated with a back end REST API. The site was a reasonably typical setup and during testing I noticed that Sourcemap files were available. The Sourcemaps turned what was a black-box pentest into a source-code assisted review, at least for the front-end. At the time, I couldn’t find an existing tool that would parse the source maps, dump the original JS source code and recreate the original directory tree. All of this is done automatically in both Chrome and Firefox’s developer tools, but I’m sure I’m not alone when I say that performing source code reviews in a browser developer console isn’t really the most pleasant experience (muh grep! etc etc).

If I can get my hands on the non-minified/uglified JavaScript for the app I’m reviewing, then that makes life easier from a testing perspective. Plus, there’s all sorts of cool stuff in there! Interesting comments, API keys, hidden administrative functionality, API endpoints that weren’t hit while clicking through the app… Definitely worth investigating while trying to hack the wider app. This article will go through how I tackled extracting the original source tree from the available source maps.

Sourcemaps

Sourcemaps are files that are often hosted alongside minified JavaScript. They allow you to map the minified JavaScript back to the original code. Effectively, the Sourcemap file allows you to punch in a line and column reference for the minified JavaScript and get back the information on which source file, line and column the minified code point maps to.

Taking a look at one of the JavaScript files used in https://reactjs.org/, here’s what that looks like. First, lets request the JavaScript file:

doi@asov:~$ curl https://reactjs.org/app-b8083d69bbb03d4954ea.js
webpackJsonp([0xd2a57dc1d883],[function(n,o,e){(function(n){"use strict";function o(n){return n&&n.__esModule?n:{default:n}}var t=Object.assign||function(n){for(var o=1;o<arguments.length;o++){var e=arguments[o];for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&(n[t]=e[t])}return n},r=e(66),c=e(2),s=(o(c),e(43)),a=o(s),l=e(63),u=e(664),i=e(312),m=o(i),d=e(15),p=e(167),h=o(p),f=e(47),g=o(f),y=e(1007),j=o(y),x=e(1008),b=o(x),P=e(165),C=o(P),N=e(164),k=o(N),w=e(104),v=o(w);e(231),window.___history=h.default,window.___emitter=g.default,v.default.addPagesArray(j.default),v.default.addProdRequires(k.default),window.asyncRequires=k.default,window.___loader=v.default,window.matchPath=l.matchPath;var I=b.default.reduce(function(n,o){return n[o.fromPath]=o,n},{}),B=function(n){var o=I[n];return null!=o&&(h.default.replace(o.toPath),!0)};B(window.location.pathname),(0,r.apiRunnerAsync)("onClientEntry").then(function(){function o(n){window.___history&&p!==!1||(window.___history=n,p=!0,n.listen(function(n,o){B(n.pathname)||setTimeout(function(){(0,r.apiRunner)("onRouteUpdate",{location:n,action:o})},0)}))}function s(n,o){var e=o.location.pathname,t=(0,r.apiRunner)("shouldUpdateScroll",{prevRouterProps:n,pathname:e});if(t.length>0)return t[0];if(n){var c=n.location.pathname;if(c===e)return!1}return!0}(0
{snip}
//# sourceMappingURL=app-562c8cc4a65111bcd19f.js.map

The JavaScript is minified and painful to read. We can retrieve the Sourcemap by appending .map to the URL:

doi@asov:~$ curl https://reactjs.org/app-562c8cc4a65111bcd19f.js.map
{"version":3,"sources":["webpack:///app-562c8cc4a65111bcd19f.js","webpack:///./.cache/production-app.js","webpack:///./~/gatsby-module-loader/patch.js","webpack:///./.cache/emitter.js","webpack:///./.cache/api-runner-browser.js","webpack:///./.cache/loader.js","webpack:///./.cache/strip-prefix.js","webpack:///./.cache/async-requires.js","webpack:///./.cache/component-renderer.js","webpack:///./.cache/find-page.js","webpack:///./.cache/history.js","webpack:///./.cache/prefetcher.js","webpack:///./.cache/register-service-worker.js","webpack:///./~/domready/ready.js","webpack:///./.cache/layouts/index.js?27db","webpack:///./src/pages/404.js?c3dd","webpack:///./src/pages/acknowledgements.html.js?e1ea","webpack:///./src/pages/blog/all.html.js?c48d","webpack:///./src/pages/docs/error-decoder.html.js?452e","webpack:///./src/pages/index.js?2b35","webpack:///./src/pages/jsx-compiler.html.js?5e3f","webpack:///./src/pages/versions.js?108f","webpack:///./src/templates/blog.js?1039","webpack:///./src/templates/codepen-example.js?dae3","webpack:///./src/templates/community.js?18b3","webpack:///./src/tem

and so on...

The Sourcemap file contains a whole bunch of information to turn the original minified, eye-bleed worthy JavaScript into something understandable by us meat bags. If you browse to https://reactjs.org/ and fire up the Chrome developer tools, you should be greeted with the original JavaScript:

chrome pretty source

Ryan Seddon has put together a helpful list of Sourcemap resources here: https://github.com/ryanseddon/source-map/wiki/Source-maps:-languages,-tools-and-other-info)

Retrieving the Original Sourcecode

The image above shows the original JavaScript source tree, which is great! That’s exactly what I’m after. Unfortunately, neither Chrome nor Firefox support a right-click-save-all function. For a smaller site, you could view each file and save it manually, but I’d prefer to have something do this programatically.

There are two arrays that I’m interested in the Sourcemap file. The sources array, which details the original source paths and the sourcesContent array, which contains the original sourcecode. This pretty much seems like an exercise in:

  • Request some JSON
  • Parse said JSON
  • Save the results into the appropriate files

All relatively elementary and a great excuse to write some crummy Golang. Full source is available here.

The end result was a simple tool that would request a Soucemap URL, parse the path information in the Sources array and recreate the source tree. The following shows an example of the tool being run against a source map from dockerhub:

doi@asov:~$ ./sourcemapper -o dhubsrc -u https://hub.docker.com/public/js/client.356c14916fb23f85707f.js.map
[+] Retriving Sourcemap from https://hub.docker.com/public/js/client.356c14916fb23f85707f.js.map
[+] Read 23045027 bytes, parsing JSON
[+] Retrieved Sourcemap with version 3, containing 1828 entries
[+] Writing 9076765 bytes to dhubsrc/webpack:/js/client.356c14916fb23f85707f.js
[+] Writing 1014 bytes to dhubsrc/webpack:/webpack/bootstrap 356c14916fb23f85707f
[+] Writing 3174 bytes to dhubsrc/webpack:/app/scripts/client.js
[+] Writing 281 bytes to dhubsrc/webpack:/~/babel-runtime/helpers/interop-require-default.js
[+] Writing 151 bytes to dhubsrc/webpack:/~/babel-core/polyfill.js
{snip}
[+] Writing 271 bytes to dhubsrc/webpack:/~/rc-tooltip/~/core-js/library/fn/object/set-prototype-of.js
[+] Writing 315 bytes to dhubsrc/webpack:/~/rc-tooltip/~/core-js/library/modules/es6.object.set-prototype-of.js
[+] Writing 1044 bytes to dhubsrc/webpack:/~/rc-tooltip/~/core-js/library/modules/_set-proto.js
[+] Writing 308 bytes to dhubsrc/webpack:/~/rc-tooltip/~/core-js/library/fn/object/create.js
[+] Writing 307 bytes to dhubsrc/webpack:/~/rc-tooltip/~/core-js/library/modules/es6.object.create.js
[+] Writing 360 bytes to dhubsrc/webpack:/~/rc-animate/~/core-js/library/fn/object/define-property.js
[+] Writing 371 bytes to dhubsrc/webpack:/~/rc-animate/~/core-js/library/modules/es6.object.define-property.js
[+] Writing 1041 bytes to dhubsrc/webpack:/~/rc-animate/~/babel-runtime/helpers/createClass.js
[+] done
doi@asov:~$ cd dhubsrc/
doi@asov:~/dhubsrc$ du -hs .
20M     .
doi@asov:~/dhubsrc$ cd webpack\:/
~/         app/       js/        webpack/   (webpack)/
doi@asov:~/dhubsrc$ cd webpack\:/app/scripts/
actions/     components/  middlewares/ reducers/    selectors/   stores/      vendor/
doi@asov:~/dhubsrc$ cd webpack\:/app/scripts/components/
doi@asov:~/dhubsrc/webpack:/app/scripts/components$ ack '/\*'
Explore.jsx
14:  /**

Routes.jsx
88:    {/* Login and Password */}
94:    {/* Currently logged in user Dashboard */}
101:    {/* Public user profile */}
107:    {/* Organization Dashboard */}
117:    {/* Organizations Summary and Add Route */}
123:    {/* Add a repository route */}
126:    {/* Autobuild creation related routes */}
139:    {/* Github linking related route | the scope selection screen */}
142:    {/* Official repositories route | TODO: add library/:name */}
149:    {/* THIS ROUTE IS A DUPLICATE OF /u/:user/. WHY IS THIS HERE??? */}
154:    <Route name="repo" path="/r/:user/*/" component={RepositoryPageWrapper}>
171:    {/* User Account Settings */}
188:    {/* Billing/Enterprise/Subscription related routes */}
191:    {/* TODO: @camacho 2/9/16 - remove routes after 1 week to give time for loaded clients to be updated*/}
201:    {/* Some publicly available routes to explore, search and ask for help */}
{snip}
doi@asov:~/dhubsrc/webpack:/app/scripts/components$ cd ../../../
doi@asov:~/dhubsrc/webpack:$ find . | wc -l
2150

In case you were wondering, that dockerhub Sourcemap file was ~22M.

At this point I can use the usual suite of unix command line tools to dig around the extracted files and look for interesting code.

Recommendations

From a security perspective, there are generally two schools of thought for something like exposed Sourcemaps or non-obfuscated JavaScript. On one hand, having your front-end code easily understandable makes debugging easier and lets you focus on writing secure software, as opposed to implementing security-through-obscurity mechanisms. The code is also easily readable (I mean compared to uglified JS! I know, I know, I’ve read horrible JavaScript written by humans as well) and verifiable by those who are interested in such things, be their intentions benevolent or malicious.

On the other hand, it makes finding vulnerabilities easier for a potential attacker. Bare in mind that if there are vulnerabilities lurking in the code base, then minification wont resolve these. There is nothing stopping an attacker from beautifying the JavaScript and digging through the code anyway. The bugs are still there, they’re just a bit more frustrating to find. By not exposing Sourcemaps in production, you can make a potential attacker’s life slightly harder.

Is it really a security issue though? I guess that’s up to you to decide for yourself. What’s the threat model say?