Skip to content

Bug(keys-manager): CLI extract can pollute Object.prototype when merging existing translation JSON #978

Description

@Dremig

Is there an existing issue for this?

  • I have searched the existing issues

Is this a regression?

No

Current behavior

@jsverse/transloco-keys-manager@8.1.0 appears to allow prototype pollution during the public CLI extract workflow when it merges existing translation JSON files.

The issue is reachable through the public CLI:

transloco-keys-manager extract -i src -o i18n -l en

When an existing translation file such as i18n/en.json contains an own __proto__ property, the CLI reads it as the current translation and later merges it through mergeDeep({}, translation, currentTranslation).

Relevant distributed code:

// keys-builder/utils/get-current-translation.js
function parseJson(path) {
  return fs.readJsonSync(path, { throws: false }) || {};
}

// keys-builder/utils/create-translation.js
function resolveTranslation({ currentTranslation, translation, replace, removeExtraKeys: removeExtraKeysParam }) {
  if (replace) {
    return mergeDeep({}, translation);
  }
  if (removeExtraKeysParam) {
    currentTranslation = removeExtraKeys(currentTranslation, translation);
  }
  return mergeDeep({}, translation, currentTranslation);
}

// utils/object.utils.js
export function mergeDeep(target, ...sources) {
  if (!sources.length) return target;
  const source = sources.shift();
  if (isObject(target) && isObject(source)) {
    for (const key in source) {
      if (isObject(source[key])) {
        if (!target[key]) Object.assign(target, { [key]: {} });
        mergeDeep(target[key], source[key]);
      } else {
        Object.assign(target, { [key]: source[key] });
      }
    }
  }
  return mergeDeep(target, ...sources);
}

There does not appear to be a guard for prototype-pollution keys such as:

__proto__
constructor
prototype

When key === "__proto__", target[key] resolves to Object.prototype for a normal object target. The recursive call then writes attacker-controlled properties onto Object.prototype.

Minimal reproduction:

Create these files:

package.json

{
  "name": "transloco-keys-manager-pp-repro",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "NODE_OPTIONS='--import ./observe.mjs' transloco-keys-manager extract -i src -o i18n -l en"
  },
  "dependencies": {
    "@jsverse/transloco-keys-manager": "8.1.0"
  }
}

src/app.ts

import { marker } from "@jsverse/transloco-keys-manager";

marker("safe.key");

i18n/en.json

{
  "__proto__": {
    "translocoPolluted": "yes"
  }
}

observe.mjs

process.on("exit", () => {
  console.log("Object.prototype.translocoPolluted:", Object.prototype.translocoPolluted);
  console.log("plain object inherited:", ({}).translocoPolluted);
  delete Object.prototype.translocoPolluted;
});

Then run:

npm install
npm test

Observed output:

Object.prototype.translocoPolluted: yes
plain object inherited: yes

The NODE_OPTIONS=--import ./observe.mjs hook is only used to observe the CLI process before it exits. The vulnerable behavior is triggered by the public transloco-keys-manager extract command, not by importing internal package paths.

Expected behavior

Translation files should be treated as data only. Existing translation JSON containing keys such as __proto__, constructor, or prototype should not modify Object.prototype or other inherited prototypes during extract.

The expected output for the reproduction should be:

Object.prototype.translocoPolluted: undefined
plain object inherited: undefined

Please provide a link to a minimal reproduction of the bug

Minimal reproduction files are shown above. If required, I can also provide them as a small public repository or attached zip.

Transloco Config

No custom Transloco configuration is required.

Debug Logs

The reproduction can be run with debug mode as well. The core issue is visible from the final observed output:


Object.prototype.translocoPolluted: yes
plain object inherited: yes

Please provide the environment you discovered this bug in

Transloco: not required for this reproduction
Transloco Keys Manager: `@jsverse/transloco-keys-manager@8.1.0`
Angular: not required for this reproduction
Node: `v24.3.0`
Package Manager: `npm`
OS: macOS

Additional context

The direct impact is prototype pollution inside the transloco-keys-manager CLI process.

This is not a standalone remote code execution issue by itself. The practical impact depends on whether a project or CI job runs the keys manager against untrusted or partially trusted translation files, for example from a pull request, localization contribution, generated translation artifact, or compromised project file.

Prototype pollution can cause application-specific effects such as unexpected option changes, denial of service, or unsafe behavior if polluted properties are later consumed by other code in the same process. In my local test, installing prettier caused the polluted prototype state to affect the prettier step with:

Failed to run prettier Getter must be a function: yes

Suggested fix:
Reject prototype-pollution keys before reading from or assigning to dynamic object keys in mergeDeep(), including:

__proto__
constructor
prototype

It would also be safer to avoid for...in for untrusted objects and use own-property iteration only, for example Object.keys() / Object.entries(), combined with the dangerous-key guard.

GitHub currently shows no security policy detected for this repository:

https://github.com/jsverse/transloco-keys-manager/security

Would you consider adding a SECURITY.md file and enabling GitHub Private Vulnerability Reporting so future security-sensitive reports can be submitted privately?

I would like to make a pull request for this bug:
Not immediately. I am happy to validate a patch or help with a small follow-up if useful.

I would like to make a pull request for this bug

Yes 🚀

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: extractKey extractionbugSomething isn't workingkeys-managerRelated to the @jsverse/transloco-keys-manager packagesecurity

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions