Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
jackfromeast opened this issue Sep 30, 2024 · 2 comments

Comments

@jackfromeast
Copy link

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
Copy link
Owner

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

jackfromeast added a commit to jackfromeast/pace that referenced this issue Oct 10, 2024
@jackfromeast
Copy link
Author

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants