pace
pace copied to clipboard
Prototype pollution vulnerability found in pace-js that leads by html injection
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;
};