Skip to content

Commit 5b8333c

Browse files
committed
Reject __proto__ as a flat messages() code, closing a sibling of GHSA-gg4h-3hg2-grpc
compile()/merge() in lib/messages.js were patched in GHSA-6w3j-5fw6-r9vr so a language named __proto__ can no longer overwrite the global Object.prototype. Both functions have a second write path that fix didn't reach: the flat, non-language error-code branch (target[code] = message / target[code] = new Template(message)). A top-level messages() key of __proto__ mapped to a string or template replaces the returned messages object's own prototype, the same confined-impact class already fixed for rename() in GHSA-gg4h-3hg2-grpc, in a sibling function that fix never touched. A language-scoped sub-key named __proto__ has the identical issue one level deeper. Reject __proto__ at each flat-write site in both compile() and merge(), mirroring the approach already used for rename(). New PoC test file locks in the fix at the Messages module level and through the public .messages()/.prefs() API. Full suite (1837 tests) passes after. Issue: #3150
1 parent e30b50e commit 5b8333c

2 files changed

Lines changed: 63 additions & 0 deletions

File tree

‎lib/messages.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,19 @@ exports.compile = function (messages, target) {
3636
if (code === 'root' ||
3737
Template.isTemplate(message)) {
3838

39+
// A flat code named __proto__ triggers the legacy accessor on plain-object
40+
// assignment, replacing target's own prototype instead of creating an own property
41+
42+
assert(code !== '__proto__', 'Cannot use __proto__ as a message code');
43+
3944
target[code] = message;
4045
continue;
4146
}
4247

4348
if (typeof message === 'string') {
49+
50+
assert(code !== '__proto__', 'Cannot use __proto__ as a message code');
51+
4452
target[code] = new Template(message);
4553
continue;
4654
}
@@ -59,6 +67,8 @@ exports.compile = function (messages, target) {
5967
for (const key of Object.keys(message)) {
6068
const localized = message[key];
6169

70+
assert(key !== '__proto__', 'Cannot use __proto__ as a message code');
71+
6272
if (key === 'root' ||
6373
Template.isTemplate(localized)) {
6474

@@ -146,11 +156,18 @@ exports.merge = function (base, extended) {
146156
if (code === 'root' ||
147157
Template.isTemplate(message)) {
148158

159+
// Same as in compile(), a flat code named __proto__ replaces target's own prototype
160+
161+
assert(code !== '__proto__', 'Cannot use __proto__ as a message code');
162+
149163
target[code] = message;
150164
continue;
151165
}
152166

153167
if (typeof message === 'string') {
168+
169+
assert(code !== '__proto__', 'Cannot use __proto__ as a message code');
170+
154171
target[code] = new Template(message);
155172
continue;
156173
}
@@ -169,6 +186,8 @@ exports.merge = function (base, extended) {
169186
for (const key of Object.keys(message)) {
170187
const localized = message[key];
171188

189+
assert(key !== '__proto__', 'Cannot use __proto__ as a message code');
190+
172191
if (key === 'root' ||
173192
Template.isTemplate(localized)) {
174193

‎test/messages_proto_poc.js‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use strict';
2+
3+
const Code = require('@hapi/code');
4+
const Lab = require('@hapi/lab');
5+
6+
const Joi = require('..');
7+
const Messages = require('../lib/messages');
8+
9+
const { describe, it } = exports.lab = Lab.script();
10+
const expect = Code.expect;
11+
12+
// JSON.parse produces a genuine own "__proto__" property (unlike the { __proto__: ... }
13+
// object literal shorthand, which the parser special-cases as prototype-setting syntax
14+
// and never creates an own key), matching how an attacker delivers this in practice
15+
// (a JSON request body or config file fed into .messages()/.prefs()).
16+
const attackerJson = (message) => JSON.parse(`{"__proto__": ${JSON.stringify(message)}}`);
17+
18+
19+
describe('messages() proto guard', () => {
20+
21+
it('does not let a top-level __proto__ code hijack the compiled messages object prototype', () => {
22+
23+
const before = Messages.compile({ 'number.min': 'too small' });
24+
expect(Object.getPrototypeOf(before)).to.equal(Object.prototype);
25+
26+
expect(() => Messages.compile(attackerJson('pwned'))).to.throw();
27+
});
28+
29+
it('rejects __proto__ as a language-scoped error code', () => {
30+
31+
expect(() => Messages.compile({ english: attackerJson('pwned') })).to.throw();
32+
});
33+
34+
it('rejects __proto__ via merge()', () => {
35+
36+
expect(() => Messages.merge({ 'number.min': 'too small' }, attackerJson('pwned'))).to.throw();
37+
});
38+
39+
it('rejects __proto__ through the public .messages()/.prefs() schema API', () => {
40+
41+
expect(() => Joi.any().messages(attackerJson('pwned'))).to.throw();
42+
expect(() => Joi.any().prefs({ messages: attackerJson('pwned') })).to.throw();
43+
});
44+
});

0 commit comments

Comments
 (0)