CodeByZach / pace

Automatically add a progress bar to your site.
https://codebyzach.github.io/pace/
MIT License
15.68k stars 1.9k forks source link

Prototype pollution vulnerability found in pace-js that leads by html injection #546

Open jackfromeast opened 1 month ago

jackfromeast commented 1 month ago

Hi, pace developers!

Summary

I have discovered a prototype pollution vulnerability in the pace-js package, which can be exploited via attacker-controlled scriptless HTML elements on web pages. This vulnerability allows attackers to manipulate the object's root prototype (i.e., Object.prototype), potentially leading to severe consequences, including Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF) on the client side if the gadget exists.

Details

Backgrounds

Prototype pollution is a type of object injection vulnerability in JavaScript that enables attackers to inject or modify properties in a prototypical object (e.g., Object.prototype). This manipulation can affect the normal execution (e.g., control- and data-flows) of a vulnerable program, potentially leading to severe consequences such as CSRF or XSS.

For more context on prototype pollution, refer to the following resources:

[1] https://yinzhicao.org/ProbetheProto/ProbetheProto.pdf
[2] https://github.com/BlackFan/client-side-prototype-pollution/tree/master

Prototype pollution vulnerability in pace-js

The pace-js package builds its configuration options by merging data from three sources: defaultOptions, window.paceOptions, and DOM elements with data-pace-options as the id. This is done using the following extend function:

https://github.com/CodeByZach/pace/blob/master/pace.js#L240

options = Pace.options = extend({}, defaultOptions, window.paceOptions, getFromDOM());

The vulnerability lies in the extend function, which recursively copies key-value pairs from the source object without proper validation of property names. This makes it vulnerable to prototype pollution attacks, as properties such as __proto__, constructor, and prototype are not sufficiently checked:

https://github.com/CodeByZach/pace/blob/master/pace.js#L110-L128

extend = function() {
        var key, out, source, sources, val, _i, _len;
        out = arguments[0], sources = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
        for (_i = 0, _len = sources.length; _i < _len; _i++) {
            source = sources[_i];
            if (source) {
                for (key in source) {
                    if (!__hasProp.call(source, key)) continue;
                    val = source[key];
                    if ((out[key] != null) && typeof out[key] === 'object' && (val != null) && typeof val === 'object') {
                        extend(out[key], val);
                    } else {
                        out[key] = val;
                    }
                }
            }
        }
        return out;
    };

Finally, I explain how can this vulnerability be exploited in the wild. Unlike defaultOptions and window.paceOptions, which require explicit developer configuration, the pace-js library also retrieves options from DOM elements. Attackers can inject malicious scriptless HTML element with a data-pace-options attribute (e.g., <img id="data-pace-options" data-pace-options='payload'>) to exploit this vulnerability. The injected payload will be parsed as JSON and passed to the vulnerable extend function:

Note that, this can be done through a website's feature that allows users to embed certain script-less HTML (e.g., markdown renderers, web email clients, forums) or via an HTML injection vulnerability in third-party JavaScript loaded on the page. And, most of client-side sanitizer, e.g., DOMPurify, will not sanitize the data and id attributes by default.

https://github.com/CodeByZach/pace/blob/master/pace.js#L141-L163

getFromDOM = function(key, json) {
  var data, e, el;
  if (key == null) {
    key = 'options';
  }
  if (json == null) {
    json = true;
  }
  el = document.querySelector("[data-pace-" + key + "]");
  if (!el) {
    return;
  }
  data = el.getAttribute("data-pace-" + key);
  if (!json) {
    return data;
  }
  try {
    return JSON.parse(data);
  } catch (_error) {
    e = _error;
    return typeof console !== "undefined" && console !== null ? console.error("Error parsing inline pace options", e) : void 0;
  }
};

PoC

<html>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pace-js@latest/pace-theme-default.min.css">
<body>
  <img id="data-pace-options" data-pace-options='{"__proto__": {"polluted": "YOU ARE POLLUTED!"}}'>
  <script src="https://cdn.jsdelivr.net/npm/pace-js@latest/pace.min.js"></script>
  <script>
    alert(Object.prototype.polluted);
  </script>
</body>
</html>

Impact

This vulnerability can directly lead to the root prototype (i.e., Object.prototype) manipulation on websites that include pace-js and allow users to inject certain scriptless HTML tags with improperly sanitized id and name attributes. With the existence of prototype pollution gadget, the attacker can achieve futher consequences like XSS and CSRF.

Patch

To fix this vulnerability, the extend function should be updated to exclude dangerous property names such as __proto__, constructor, and prototype:

extend = function() {
        var key, out, source, sources, val, _i, _len;
        out = arguments[0], sources = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
        for (_i = 0, _len = sources.length; _i < _len; _i++) {
            source = sources[_i];
            if (source) {
                for (key in source) {
                    if (!__hasProp.call(source, key) || key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
                    val = source[key];
                    if ((out[key] != null) && typeof out[key] === 'object' && (val != null) && typeof val === 'object') {
                        extend(out[key], val);
                    } else {
                        out[key] = val;
                    }
                }
            }
        }
        return out;
    };
CodeByZach commented 1 month ago

Hi @jackfromeast! Thank you for the thorough analysis. Feel free to submit a PR for review. Cheers.

jackfromeast commented 1 month ago

Thank you, @CodeByZach! I'd be happy to open a pull request to address this vulnerability.

Could you please let me know which minification tool and version were used for pace-js so I can ensure consistency in my updates?

Also, could you enable the private security reporting feature in the GitHub settings for this repository? This would allow me to open a security advisory for this issue, making it easier for others to reference.

Thanks in advance!