Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lib/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ exports.compile = function (messages, target) {
if (code === 'root' ||
Template.isTemplate(message)) {

// A flat code named __proto__ triggers the legacy accessor on plain-object
// assignment, replacing target's own prototype instead of creating an own property
// A flat code named __proto__ triggers the legacy accessor on plain-object assignment, and
// what we assign is always a Template, i.e. an object the accessor accepts, so it replaces
// target's own prototype instead of creating an own property. That in turn makes
// Template.isTemplate(target) true, so every code renders that one message

assert(code !== '__proto__', 'Cannot use __proto__ as a message code');

Expand Down
24 changes: 24 additions & 0 deletions test/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -2108,6 +2108,30 @@ describe('any', () => {
}
});

it('rejects a flat message code named __proto__', () => {

// The message is wrapped in a Template before the assignment, and a Template is an object,
// so the inherited setter takes it and replaces the returned object's own prototype. That
// makes Template.isTemplate() true for the whole messages object, so every code renders the
// attacker's message and the next compile() throws 'Cannot set single message template'

for (const message of ['pwned', Joi.x('pwned')]) {
const messages = { ['__proto__']: message };

expect(() => Joi.number().min(10).messages(messages)).to.throw('Cannot use __proto__ as a message code');
expect(() => Joi.number().min(10).prefs({ messages })).to.throw('Cannot use __proto__ as a message code');
expect(() => Joi.number().min(10).validate(1, { messages })).to.throw('Cannot use __proto__ as a message code');
}
});

it('rejects a language scoped message code named __proto__', () => {

const messages = { english: { ['__proto__']: 'pwned' } };

expect(() => Joi.number().min(10).messages(messages)).to.throw('Cannot use __proto__ as a message code');
expect(() => Joi.number().min(10).messages({ english: { ['__proto__']: Joi.x('pwned') } })).to.throw('Cannot use __proto__ as a message code');
});

it('errors on invalid message value', () => {

expect(() => Joi.number().min(10).message(12)).to.throw('Invalid message options');
Expand Down
18 changes: 18 additions & 0 deletions test/extend.js
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,24 @@ describe('extension', () => {
}
});

it('rejects a flat message code named __proto__', () => {

// Same as compile(), the Template lands on the merged object's own prototype

for (const message of ['pwned', Joi.x('pwned')]) {
const extend = () => Joi.extend({ type: 'special', base: Joi.string(), messages: { ['__proto__']: message } });

expect(extend).to.throw('Cannot use __proto__ as a message code');
}
});

it('rejects a language scoped message code named __proto__', () => {

const extend = () => Joi.extend({ type: 'special', base: Joi.string(), messages: { english: { ['__proto__']: 'pwned' } } });

expect(extend).to.throw('Cannot use __proto__ as a message code');
});

it('overrides specific error messages with template', () => {

const custom = Joi.extend({
Expand Down
44 changes: 0 additions & 44 deletions test/messages_proto_poc.js

This file was deleted.

Loading