Skip to content

Commit 3f3907c

Browse files
authored
Merge pull request #3135 from hapijs/chore/backport-rename-proto
chore: backport prevent proto on renames
2 parents 3d3ab76 + 172ecec commit 3f3907c

3 files changed

Lines changed: 75 additions & 2 deletions

File tree

‎API.md‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2550,7 +2550,7 @@ const value = await Joi.compile(schema).validateAsync(input);
25502550
// value === { x123x: 'x', x1x: 'y', x0x: 'z', x4x: 'test' }
25512551
```
25522552

2553-
Possible validation errors: [`object.rename.multiple`](#objectrenamemultiple), [`object.rename.override`](#objectrenameoverride)
2553+
Possible validation errors: [`object.rename.multiple`](#objectrenamemultiple), [`object.rename.override`](#objectrenameoverride), [`object.rename.proto`](#objectrenameproto)
25542554

25552555
#### `object.schema([type])`
25562556

@@ -4145,6 +4145,19 @@ Additional local context properties:
41454145
}
41464146
```
41474147

4148+
#### `object.rename.proto`
4149+
4150+
The target property is `__proto__`, which would set the object prototype instead of creating a key.
4151+
4152+
Additional local context properties:
4153+
```ts
4154+
{
4155+
from: string, // Origin property name of the rename
4156+
to: string, // Target property of the rename
4157+
pattern: boolean // Indicates if the rename source was a pattern (regular expression)
4158+
}
4159+
```
4160+
41484161
#### `object.schema`
41494162

41504163
The object was not a **joi** schema.

‎lib/types/keys.js‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,7 @@ module.exports = Any.extend({
547547
'object.regex': '{{#label}} must be a RegExp object',
548548
'object.rename.multiple': '{{#label}} cannot rename {{:#from}} because multiple renames are disabled and another key was already renamed to {{:#to}}',
549549
'object.rename.override': '{{#label}} cannot rename {{:#from}} because override is disabled and target {{:#to}} exists',
550+
'object.rename.proto': '{{#label}} cannot rename {{:#from}} because target {{:#to}} is a reserved key',
550551
'object.schema': '{{#label}} must be a Joi schema of {{#type}} type',
551552
'object.unknown': '{{#label}} is not allowed',
552553
'object.with': '{{:#mainWithLabel}} missing required peer {{:#peerWithLabel}}',
@@ -567,8 +568,15 @@ internals.clone = function (value, prefs) {
567568
return Clone(value, { shallow: true });
568569
}
569570

570-
const clone = Object.create(Object.getPrototypeOf(value));
571+
const proto = Object.getPrototypeOf(value);
572+
const clone = Object.create(proto);
571573
Object.assign(clone, value);
574+
575+
// Restore the prototype in case of pre-existing prototype pollution
576+
if (Object.getPrototypeOf(clone) !== proto) {
577+
Object.setPrototypeOf(clone, proto);
578+
}
579+
572580
return clone;
573581
}
574582

@@ -867,6 +875,15 @@ internals.rename = function (schema, value, state, prefs, errors) {
867875
}
868876
}
869877

878+
if (to === '__proto__') {
879+
errors.push(schema.$_createError('object.rename.proto', value, { from, to, pattern }, state, prefs));
880+
if (prefs.abortEarly) {
881+
return false;
882+
}
883+
884+
continue;
885+
}
886+
870887
if (value[from] === undefined) {
871888
delete value[to];
872889
}

‎test/types/object.js‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,28 @@ describe('object', () => {
105105
expect(schema.validate(new Test()).value).to.be.instanceof(Test);
106106
});
107107

108+
it('does not pollute the cloned value prototype via a __proto__ key', () => {
109+
110+
const schema = Joi.object({ name: Joi.string() });
111+
112+
const payload = JSON.parse('{"name":"alice","__proto__":{"isAdmin":true,"role":"superuser"}}');
113+
expect(Object.getPrototypeOf(payload)).to.equal(Object.prototype);
114+
expect(payload.isAdmin).to.not.exist();
115+
116+
const { value, error } = schema.validate(payload);
117+
expect(error).to.not.exist();
118+
expect(Object.getPrototypeOf(value)).to.equal(Object.prototype);
119+
expect(value.isAdmin).to.not.exist();
120+
expect(value.role).to.not.exist();
121+
122+
// Same with allowUnknown disabled
123+
124+
const { value: value2, error: error2 } = schema.validate(payload, { allowUnknown: false });
125+
expect(error2).to.not.exist();
126+
expect(Object.getPrototypeOf(value2)).to.equal(Object.prototype);
127+
expect(value2.isAdmin).to.not.exist();
128+
});
129+
108130
it('allows any key when schema is undefined', () => {
109131

110132
Helper.validate(Joi.object(), [[{ a: 4 }, true]]);
@@ -3031,6 +3053,27 @@ describe('object', () => {
30313053
Helper.validate(Joi.compile(schema), [[{ other: 'here', A: 100, c: 50 }, true, { other: 'here', A: 100, b: 100, c: 50 }]]);
30323054
});
30333055

3056+
it('errors on a template target rendering __proto__ instead of setting the prototype', () => {
3057+
3058+
const payload = JSON.parse('{"user":"alice","x-__proto__":{"isAdmin":true,"role":"superuser"}}');
3059+
3060+
const schema = Joi.object({ user: Joi.string().required() })
3061+
.rename(/^x-(.+)$/, Joi.x('{#1}'), { multiple: true })
3062+
.unknown(false);
3063+
3064+
const { value, error } = schema.validate(payload);
3065+
expect(error).to.be.an.error('"value" cannot rename "x-__proto__" because target "__proto__" is a reserved key');
3066+
expect(error.details[0].type).to.equal('object.rename.proto');
3067+
expect(Object.getPrototypeOf(value)).to.equal(Object.prototype);
3068+
expect(value.isAdmin).to.not.exist();
3069+
expect(Object.prototype.isAdmin).to.not.exist();
3070+
3071+
const { value: value2, error: error2 } = schema.validate(payload, { abortEarly: false });
3072+
expect(error2.details[0].type).to.equal('object.rename.proto');
3073+
expect(Object.getPrototypeOf(value2)).to.equal(Object.prototype);
3074+
expect(value2.isAdmin).to.not.exist();
3075+
});
3076+
30343077
it('uses template', () => {
30353078

30363079
const schema = Joi.object()

0 commit comments

Comments
 (0)