Skip to content

WRITING / POSTS

Webpack Front-End Penetration Testing: Sourcemaps and Exposed APIs

A Webpack penetration-testing guide to identifying bundles, recovering source with sourcemaps, and investigating sensitive APIs and exposed configuration.

On this page 6 sections

Webpack Penetration Testing Approach

This is what I’ve learned so far. Webpack is a static module bundler for modern JavaScript applications. When webpack processes an application, it internally builds a dependency graph mapping every module the project needs, then generates one or more bundles.

img

So the overall approach is:

“Identify characteristics — restore source code — locate key logic — perform security analysis”

How to Identify a Webpack-Bundled Site

fofa query

body="chunk.js" || body="runtime" || body="
__webpack_require__
"
body="/static/js/" && body=".chunk.js"

Typical examples are as follows:

<!-- webpack 打包后的典型 HTML 结构 -->
<!DOCTYPE html>
<html>
<head>
    <title>App</title>
    <!-- webpack 生成的 CSS 文件,通常带有 hash -->
    <link href="/static/css/main.a1b2c3d4.css" rel="stylesheet">
</head>
<body>
    <div id="root"></div>
    <!-- webpack 生成的 JS 文件,通常带有 hash -->
    <script src="/static/js/runtime-main.1a2b3c4d.js"></script>
    <script src="/static/js/2.e5f6g7h8.chunk.js"></script>
    <script src="/static/js/main.i9j0k1l2.chunk.js"></script>
</body>
</html>
  • Filenames containing hash values: such as main.a1b2c3d4.js
  • Chunk files: filenames include the keyword chunk
  • Runtime files: there is usually a runtime file
  • Numerically named files: such as 2.e5f6g7h8.chunk.js

For example:

img

img

The Source Code in the Console Contains Webpack://

Webpack currently supports multiple debugging modes. They differ in how the compiled code maps back to the source code. Specifically, source code is mapped in different ways. Some modes generate a .map file (for source mapping), some include source map information as comments, and others use a DataUrl (embedding the mapping information in the generated file as a data URI).

Several common source-map bundling methods:

  1. source-map

Generates a separate .map file for debugging, allowing the source code to be restored during debugging.

Suitable for production environments to help troubleshoot issues, but it exposes source code.

  1. inline-source-map

Embeds the source map in the compiled file as a DataUrl and does not generate a separate .map file.

Suitable for development, making source inspection convenient but increasing file size.

  1. eval-source-map

Embeds the source map in each module through eval. It builds quickly and provides a good debugging experience.

Suitable for development; not recommended for production.

  1. cheap-module-source-map

Provides line mappings rather than column mappings, ignores loader source maps within modules, and generates quickly.

Suitable for development; improves efficiency if you only care about line numbers.

  1. hidden-source-map

Generates a .map file but does not reference it in the compiled file. Suitable when the source map is needed only for an error-reporting tool and should not be exposed directly to users.

img

There is a leak.

In Sources → Page → Webpack://, you can view the Webpack project’s source code.

You can use the following method to reverse webpack.

Then you can analyze the project structure, audit the source code, and so on.

Automated Tool Detection

https://github.com/rtcatc/Packer-Fuzzer

This tool can scan a Webpack site and probe for vulnerabilities.

python .\PackerFuzzer.py -u "..."

img

It generates a doc document with the scan report.

img

Webpack Example

The related code has been placed in a GitHub repository.

https://github.com/bx33661/Webpack-demo/tree/main

npm run dev
npm run build

img

Two files are generated in the dist folder:

  1. bundle.js

bundle.js is the JavaScript file generated by webpack; it contains all of the project’s JavaScript modules and dependencies.

Key characteristics:

  • Code compression: The file is compressed and obfuscated to reduce its size and improve load speed. That’s why it is difficult to read.
  • Module integration: webpack bundles all modules (including index.js and greeting.js) into one file, resolving module dependencies.
  • Style injection: The CSS file (style.css) is also bundled into this file, and a style tag is dynamically created and injected into the page through JavaScript.
  • Self-invoking function: The entire code is wrapped in a self-invoking function (()=>{…})(), creating an isolated scope.
  1. index.html
  • Automatic bundle inclusion: A <script defer="defer" src="bundle.js"></script> tag is automatically added to include the bundled JavaScript file.
  • HTML compression: The HTML is compressed, removing unnecessary spaces and line breaks.
  • Content structure retained: The DOM structure from the original template is preserved, including headings, buttons, and the results display area.
  • Metadata retained: The original template’s meta tags and other header information are preserved.
(() => {
  "use strict";
  var n = {
    56: (n, e, t) => {
      n.exports = function(n) {
        var e = t.nc;
        e && n.setAttribute("nonce", e)
      }
    },
    72: n => {
      var e = [];
      function t(n) {
        for (var t = -1, r = 0; r < e.length; r++)
          if (e[r].identifier === n) {
            t = r;
            break
          } return t
      }
      function r(n, r) {
        for (var a = {}, i = [], c = 0; c < n.length; c++) {
          var s = n[c],
            d = r.base ? s[0] + r.base : s[0],
            u = a[d] || 0,
            p = "".concat(d, " ").concat(u);
          a[d] = u + 1;
          var l = t(p),
            f = {
              css: s[1],
              media: s[2],
              sourceMap: s[3],
              supports: s[4],
              layer: s[5]
            };
          if (-1 !== l) e[l].references++, e[l].updater(f);
          else {
            var v = o(f, r);
            r.byIndex = c, e.splice(c, 0, {
              identifier: p,
              updater: v,
              references: 1
            })
          }
          i.push(p)
        }
        return i
      }
      function o(n, e) {
        var t = e.domAPI(e);
        return t.update(n),
          function(e) {
            if (e) {
              if (e.css === n.css && e.media === n.media && e.sourceMap === n.sourceMap && e.supports === n.supports && e.layer === n.layer) return;
              t.update(n = e)
            } else t.remove()
          }
      }
      n.exports = function(n, o) {
        var a = r(n = n || [], o = o || {});
        return function(n) {
          n = n || [];
          for (var i = 0; i < a.length; i++) {
            var c = t(a[i]);
            e[c].references--
          }
          for (var s = r(n, o), d = 0; d < a.length; d++) {
            var u = t(a[d]);
            0 === e[u].references && (e[u].updater(), e.splice(u, 1))
          }
          a = s
        }
      }
    },
    113: n => {
      n.exports = function(n, e) {
        if (e.styleSheet) e.styleSheet.cssText = n;
        else {
          for (; e.firstChild;) e.removeChild(e.firstChild);
          e.appendChild(document.createTextNode(n))
        }
      }
    },
    208: (n, e, t) => {
      t.d(e, {
        A: () => c
      });
      var r = t(601),
        o = t.n(r),
        a = t(314),
        i = t.n(a)()(o());
      i.push([n.id, "* {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: Arial, sans-serif;\n  line-height: 1.6;\n  color: #333;\n  background-color: #f4f4f4;\n}\n\n#app {\n  max-width: 800px;\n  margin: 0 auto;\n  padding: 20px;\n}\n\n.title {\n  color: #2c3e50;\n  text-align: center;\n  margin-bottom: 20px;\n  padding-bottom: 10px;\n  border-bottom: 1px solid #eee;\n}\n\n.content {\n  background: white;\n  padding: 20px;\n  border-radius: 5px;\n  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n}\n\nbutton {\n  background: #3498db;\n  color: white;\n  border: none;\n  padding: 10px 15px;\n  border-radius: 4px;\n  cursor: pointer;\n  margin-top: 10px;\n  font-size: 16px;\n  transition: background 0.3s;\n}\n\nbutton:hover {\n  background: #2980b9;\n}\n\n#result {\n  margin-top: 20px;\n  padding: 15px;\n  background: #f9f9f9;\n  border-left: 4px solid #3498db;\n  display: none;\n}\n\n#result.active {\n  display: block;\n  animation: fadeIn 0.5s;\n}\n\n@keyframes fadeIn {\n  from { opacity: 0; }\n  to { opacity: 1; }\n}", ""]);
                const c = i
            },
            314: n => {
                n.exports = function(n) {
                    var e = [];
                    return e.toString = function() {
                        return this.map((function(e) {
                            var t = "",
                                r = void 0 !== e[5];
                            return e[4] && (t += "@supports (".concat(e[4], ") {")), e[2] && (t += "@media ".concat(e[2], " {")), r && (t += "@layer".concat(e[5].length > 0 ? " ".concat(e[5]) : "", " {")), t += n(e), r && (t += "}"), e[2] && (t += "}"), e[4] && (t += "}"), t
                        })).join("")
                    }, e.i = function(n, t, r, o, a) {
                        "string" == typeof n && (n = [
                            [null, n, void 0]
                        ]);
                        var i = {};
                        if (r)
                            for (var c = 0; c < this.length; c++) {
                                var s = this[c][0];
                                null != s && (i[s] = !0)
                            }
                        for (var d = 0; d < n.length; d++) {
                            var u = [].concat(n[d]);
                            r && i[u[0]] || (void 0 !== a && (void 0 === u[5] || (u[1] = "@layer".concat(u[5].length > 0 ? " ".concat(u[5]) : "", " {").concat(u[1], "}")), u[5] = a), t && (u[2] ? (u[1] = "@media ".concat(u[2], " {").concat(u[1], "}"), u[2] = t) : u[2] = t), o && (u[4] ? (u[1] = "@supports (".concat(u[4], ") {").concat(u[1], "}"), u[4] = o) : u[4] = "".concat(o)), e.push(u))
                        }
                    }, e
                }
            },
            540: n => {
                n.exports = function(n) {
                    var e = document.createElement("style");
                    return n.setAttributes(e, n.attributes), n.insert(e, n.options), e
                }
            },
            601: n => {
                n.exports = function(n) {
                    return n[1]
                }
            },
            659: n => {
                var e = {};
                n.exports = function(n, t) {
                    var r = function(n) {
                        if (void 0 === e[n]) {
                            var t = document.querySelector(n);
                            if (window.HTMLIFrameElement && t instanceof window.HTMLIFrameElement) try {
                                t = t.contentDocument.head
                            } catch (n) {
                                t = null
                            }
                            e[n] = t
                        }
                        return e[n]
                    }(n);
                    if (!r) throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");
                    r.appendChild(t)
                }
            },
            825: n => {
                n.exports = function(n) {
                    if ("undefined" == typeof document) return {
                        update: function() {},
                        remove: function() {}
                    };
                    var e = n.insertStyleElement(n);
                    return {
                        update: function(t) {
                            ! function(n, e, t) {
                                var r = "";
                                t.supports && (r += "@supports (".concat(t.supports, ") {")), t.media && (r += "@media ".concat(t.media, " {"));
                                var o = void 0 !== t.layer;
                                o && (r += "@layer".concat(t.layer.length > 0 ? " ".concat(t.layer) : "", " {")), r += t.css, o && (r += "}"), t.media && (r += "}"), t.supports && (r += "}");
                                var a = t.sourceMap;
                                a && "undefined" != typeof btoa && (r += "\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(a)))), " */")), e.styleTagTransform(r, n, e.options)
                            }(e, n, t)
                        },
                        remove: function() {
                            ! function(n) {
                                if (null === n.parentNode) return !1;
                                n.parentNode.removeChild(n)
                            }(e)
                        }
                    }
                }
            }
        },
        e = {};
    function t(r) {
        var o = e[r];
        if (void 0 !== o) return o.exports;
        var a = e[r] = {
            id: r,
            exports: {}
        };
        return n[r](a, a.exports, t), a.exports
    }
    t.n = n => {
        var e = n && n.__esModule ? () => n.default : () => n;
        return t.d(e, {
            a: e
        }), e
    }, t.d = (n, e) => {
        for (var r in e) t.o(e, r) && !t.o(n, r) && Object.defineProperty(n, r, {
            enumerable: !0,
            get: e[r]
        })
    }, t.o = (n, e) => Object.prototype.hasOwnProperty.call(n, e), t.nc = void 0;
    var r = t(72),
        o = t.n(r),
        a = t(825),
        i = t.n(a),
        c = t(659),
        s = t.n(c),
        d = t(56),
        u = t.n(d),
        p = t(540),
        l = t.n(p),
        f = t(113),
        v = t.n(f),
        m = t(208),
        b = {};
    b.styleTagTransform = v(), b.setAttributes = u(), b.insert = s().bind(null, "head"), b.domAPI = i(), b.insertStyleElement = l(), o()(m.A, b), m.A && m.A.locals && m.A.locals, document.addEventListener("DOMContentLoaded", (() => {
        console.log("页面已加载");
        const n = document.getElementById("clickBtn"),
            e = document.getElementById("result");
        n && e && n.addEventListener("click", (() => {
            const n = 
你好,Webpack! 当前时间: ${(new Date).toLocaleTimeString()}
;
            e.textContent = n, e.classList.add("active")
        }))
    }))
})();

Make a change and add the devtool option:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  // 入口文件
  entry: './src/index.js',

  // 添加devtool配置来启用source map
  devtool: 'source-map', // 这将生成完整的source map文件

  // 输出配置
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true // 每次构建前清理dist文件夹
  },

  // ... 其余配置保持不变 ...
};

Then we can

❯ npm run build

This generates a bundle.js.map file.

Reverse Webpack

There are many tools; here we’ll use reverse-sourcemap.

npm install --global reverse-sourcemap

Let’s look at how to use it.

❯ reverse-sourcemap -h
reverse-sourcemap - Reverse engineering JavaScript and CSS sources from sourcemaps
Usage: reverse-sourcemap [options] <file|directory>
  -h, --help               Help and usage instructions
  -V, --version            Version number
  -v, --verbose            Verbose output, will print which file is currently being

                           processed
  -o, --output-dir String  Output directory - default: .
  -M, --match String       Regular expression for matching and filtering files -
                           default: \.map$
  -r, --recursive          Recursively search matching files
Version 1.0.4

Reverse it directly.

❯ reverse-sourcemap --output-dir ./ bundle.js.map

img

The results are as follows:

img

As you can see, the structure is basically the same as the original project structure.

img

The result is quite good.

img

So sometimes we find that a website has leaked a “map” file, and we can achieve unexpected results by exploiting it.

Edit this post

Continue reading

Related posts
Previous post
Godzilla Webshell Traffic Analysis and Attribution
Next post
Windows Incident Response: Investigating Logs, Processes, and Persistence

On this page

SEARCH / QUICK FIND

Search English content

Try searching