Skip to content

Commit d7ff140

Browse files
authored
fix: backport DOS via __proto__ key in merge config fix to v0.x (#7388)
* backport fix * add unit tests * use `require()` instead of `import`
1 parent 2fcb4ec commit d7ff140

3 files changed

Lines changed: 234 additions & 1 deletion

File tree

lib/core/mergeConfig.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ module.exports = function mergeConfig(config1, config2) {
9494
};
9595

9696
utils.forEach(Object.keys(config1).concat(Object.keys(config2)), function computeConfigValue(prop) {
97-
var merge = mergeMap[prop] || mergeDeepProperties;
97+
if (prop === '__proto__' || prop === 'constructor' || prop === 'prototype') {
98+
return;
99+
}
100+
var merge = utils.hasOwnProperty(mergeMap, prop) ? mergeMap[prop] : mergeDeepProperties;
98101
var configValue = merge(prop);
99102
(utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue);
100103
});

lib/utils.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,10 @@ function forEach(obj, fn) {
310310
function merge(/* obj1, obj2, obj3, ... */) {
311311
var result = {};
312312
function assignValue(val, key) {
313+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
314+
return;
315+
}
316+
313317
if (isPlainObject(result[key]) && isPlainObject(val)) {
314318
result[key] = merge(result[key], val);
315319
} else if (isPlainObject(val)) {
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
"use strict";
2+
3+
var assert = require('assert');
4+
var utils = require('../../../lib/utils');
5+
var mergeConfig = require('../../../lib/core/mergeConfig');
6+
7+
describe("Prototype Pollution Protection", function () {
8+
afterEach(function () {
9+
// Clean up any pollution that might have occurred
10+
delete Object.prototype.polluted;
11+
});
12+
13+
describe("utils.merge", function () {
14+
it("should filter __proto__ key at top level", function () {
15+
const result = utils.merge(
16+
{},
17+
{ __proto__: { polluted: "yes" }, safe: "value" },
18+
);
19+
20+
assert.strictEqual(Object.prototype.polluted, undefined);
21+
assert.strictEqual(result.safe, "value");
22+
assert.strictEqual(result.hasOwnProperty("__proto__"), false);
23+
});
24+
25+
it("should filter constructor key at top level", function () {
26+
const result = utils.merge(
27+
{},
28+
{ constructor: { polluted: "yes" }, safe: "value" },
29+
);
30+
31+
assert.strictEqual(result.safe, "value");
32+
assert.strictEqual(result.hasOwnProperty("constructor"), false);
33+
});
34+
35+
it("should filter prototype key at top level", function () {
36+
const result = utils.merge(
37+
{},
38+
{ prototype: { polluted: "yes" }, safe: "value" },
39+
);
40+
41+
assert.strictEqual(result.safe, "value");
42+
assert.strictEqual(result.hasOwnProperty("prototype"), false);
43+
});
44+
45+
it("should filter __proto__ key in nested objects", function () {
46+
const result = utils.merge(
47+
{},
48+
{
49+
headers: {
50+
__proto__: { polluted: "nested" },
51+
"Content-Type": "application/json",
52+
},
53+
},
54+
);
55+
56+
assert.strictEqual(Object.prototype.polluted, undefined);
57+
assert.strictEqual(result.headers["Content-Type"], "application/json");
58+
assert.strictEqual(result.headers.hasOwnProperty("__proto__"), false);
59+
});
60+
61+
it("should filter constructor key in nested objects", function () {
62+
const result = utils.merge(
63+
{},
64+
{
65+
headers: {
66+
constructor: { prototype: { polluted: "nested" } },
67+
"Content-Type": "application/json",
68+
},
69+
},
70+
);
71+
72+
assert.strictEqual(Object.prototype.polluted, undefined);
73+
assert.strictEqual(result.headers["Content-Type"], "application/json");
74+
assert.strictEqual(result.headers.hasOwnProperty("constructor"), false);
75+
});
76+
77+
it("should filter prototype key in nested objects", function () {
78+
const result = utils.merge(
79+
{},
80+
{
81+
headers: {
82+
prototype: { polluted: "nested" },
83+
"Content-Type": "application/json",
84+
},
85+
},
86+
);
87+
88+
assert.strictEqual(result.headers["Content-Type"], "application/json");
89+
assert.strictEqual(result.headers.hasOwnProperty("prototype"), false);
90+
});
91+
92+
it("should filter dangerous keys in deeply nested objects", function () {
93+
const result = utils.merge(
94+
{},
95+
{
96+
level1: {
97+
level2: {
98+
__proto__: { polluted: "deep" },
99+
prototype: { polluted: "deep" },
100+
safe: "value",
101+
},
102+
},
103+
},
104+
);
105+
106+
assert.strictEqual(Object.prototype.polluted, undefined);
107+
assert.strictEqual(result.level1.level2.safe, "value");
108+
assert.strictEqual(
109+
result.level1.level2.hasOwnProperty("__proto__"),
110+
false,
111+
);
112+
});
113+
114+
it("should still merge regular properties correctly", function () {
115+
const result = utils.merge({ a: 1, b: { c: 2 } }, { b: { d: 3 }, e: 4 });
116+
117+
assert.strictEqual(result.a, 1);
118+
assert.strictEqual(result.b.c, 2);
119+
assert.strictEqual(result.b.d, 3);
120+
assert.strictEqual(result.e, 4);
121+
});
122+
123+
it("should handle JSON.parse payloads safely", function () {
124+
const malicious = JSON.parse('{"__proto__": {"polluted": "yes"}}');
125+
const result = utils.merge({}, malicious);
126+
127+
assert.strictEqual(Object.prototype.polluted, undefined);
128+
assert.strictEqual(result.hasOwnProperty("__proto__"), false);
129+
});
130+
131+
it("should handle nested JSON.parse payloads safely", function () {
132+
const malicious = JSON.parse(
133+
'{"headers": {"constructor": {"prototype": {"polluted": "yes"}}}}',
134+
);
135+
const result = utils.merge({}, malicious);
136+
137+
assert.strictEqual(Object.prototype.polluted, undefined);
138+
assert.strictEqual(result.headers.hasOwnProperty("constructor"), false);
139+
});
140+
});
141+
142+
describe("mergeConfig", function () {
143+
it("should filter dangerous keys at top level", function () {
144+
const result = mergeConfig(
145+
{},
146+
{
147+
__proto__: { polluted: "yes" },
148+
constructor: { polluted: "yes" },
149+
prototype: { polluted: "yes" },
150+
url: "/api/test",
151+
},
152+
);
153+
154+
assert.strictEqual(Object.prototype.polluted, undefined);
155+
assert.strictEqual(result.url, "/api/test");
156+
assert.strictEqual(result.hasOwnProperty("__proto__"), false);
157+
assert.strictEqual(result.hasOwnProperty("constructor"), false);
158+
assert.strictEqual(result.hasOwnProperty("prototype"), false);
159+
});
160+
161+
it("should filter dangerous keys in headers", function () {
162+
const result = mergeConfig(
163+
{},
164+
{
165+
headers: {
166+
__proto__: { polluted: "yes" },
167+
"Content-Type": "application/json",
168+
},
169+
},
170+
);
171+
172+
assert.strictEqual(Object.prototype.polluted, undefined);
173+
assert.strictEqual(result.headers["Content-Type"], "application/json");
174+
assert.strictEqual(result.headers.hasOwnProperty("__proto__"), false);
175+
});
176+
177+
it("should filter dangerous keys in custom config properties", function () {
178+
const result = mergeConfig(
179+
{},
180+
{
181+
customProp: {
182+
__proto__: { polluted: "yes" },
183+
safe: "value",
184+
},
185+
},
186+
);
187+
188+
assert.strictEqual(Object.prototype.polluted, undefined);
189+
assert.strictEqual(result.customProp.safe, "value");
190+
assert.strictEqual(result.customProp.hasOwnProperty("__proto__"), false);
191+
});
192+
193+
it("should still merge configs correctly", function () {
194+
const config1 = {
195+
baseURL: "https://api.example.com",
196+
timeout: 1000,
197+
headers: {
198+
common: {
199+
Accept: "application/json",
200+
},
201+
},
202+
};
203+
204+
const config2 = {
205+
url: "/users",
206+
timeout: 5000,
207+
headers: {
208+
common: {
209+
"Content-Type": "application/json",
210+
},
211+
},
212+
};
213+
214+
const result = mergeConfig(config1, config2);
215+
216+
assert.strictEqual(result.baseURL, "https://api.example.com");
217+
assert.strictEqual(result.url, "/users");
218+
assert.strictEqual(result.timeout, 5000);
219+
assert.strictEqual(result.headers.common.Accept, "application/json");
220+
assert.strictEqual(
221+
result.headers.common["Content-Type"],
222+
"application/json",
223+
);
224+
});
225+
});
226+
});

0 commit comments

Comments
 (0)