Skip to content

Commit 98287e0

Browse files
committed
Protect against prototype pollution in markup parser
1 parent 0afe3a6 commit 98287e0

3 files changed

Lines changed: 57 additions & 20 deletions

File tree

src/core/core.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ const revision = '$_CURRENT_SDK_REVISION';
3636
*/
3737
function extend(target, ex) {
3838
for (const prop in ex) {
39+
if (prop === '__proto__' || prop === 'constructor' || prop === 'prototype') {
40+
continue;
41+
}
42+
3943
const copy = ex[prop];
4044

4145
if (Array.isArray(copy)) {

src/framework/components/element/markup.js

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { extend } from '../../../core/core.js';
2+
13
// markup scanner
24

35
// list of scanner tokens
@@ -330,25 +332,6 @@ class Parser {
330332
}
331333
}
332334

333-
// copy the contents of source object into target object (like a deep version
334-
// of assign)
335-
function merge(target, source) {
336-
for (const key in source) {
337-
if (!source.hasOwnProperty(key)) {
338-
continue;
339-
}
340-
const value = source[key];
341-
if (value instanceof Object) {
342-
if (!target.hasOwnProperty(key)) {
343-
target[key] = { };
344-
}
345-
merge(target[key], source[key]);
346-
} else {
347-
target[key] = value;
348-
}
349-
}
350-
}
351-
352335
function combineTags(tags) {
353336
if (tags.length === 0) {
354337
return null;
@@ -358,7 +341,7 @@ function combineTags(tags) {
358341
const tag = tags[index];
359342
const tmp = { };
360343
tmp[tag.name] = { value: tag.value, attributes: tag.attributes };
361-
merge(result, tmp);
344+
extend(result, tmp);
362345
}
363346
return result;
364347
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { expect } from 'chai';
2+
import { extend } from '../../../../src/core/core.js';
3+
import { Markup } from '../../../../src/framework/components/element/markup.js';
4+
5+
describe('Security: Prototype Pollution', function () {
6+
describe('Markup Parser', function () {
7+
it('should ignore sensitive keys like __proto__ during merging', function () {
8+
const symbols = ['[', '_', '_', 'p', 'r', 'o', 't', 'o', '_', '_', ' ', 'p', 'o', 'l', 'l', 'u', 't', 'e', 'd', '=', '"', 't', 'r', 'u', 'e', '"', ']', 'h', 'e', 'l', 'l', 'o'];
9+
10+
// Ensure Object.prototype is clean before test
11+
expect({}.polluted).to.be.undefined;
12+
13+
try {
14+
Markup.evaluate(symbols);
15+
} catch (e) {
16+
// ignore potential errors during evaluation as we only care about pollution
17+
}
18+
19+
expect({}.polluted).to.be.undefined;
20+
});
21+
22+
it('should ignore constructor and prototype keys', function () {
23+
const symbols = ['[', 'c', 'o', 'n', 's', 't', 'r', 'u', 'c', 't', 'o', 'r', ']', 't', 'e', 's', 't'];
24+
25+
const results = Markup.evaluate(symbols);
26+
expect(results.tags).to.not.have.property('constructor');
27+
});
28+
});
29+
30+
describe('Core: extend utility', function () {
31+
it('should protect against prototype pollution', function () {
32+
const payload = JSON.parse('{"__proto__": {"vulnerable": "yes"}}');
33+
const target = {};
34+
35+
extend(target, payload);
36+
37+
expect({}.vulnerable).to.be.undefined;
38+
expect(target).to.not.have.property('__proto__');
39+
});
40+
41+
it('should protect against constructor/prototype pollution', function () {
42+
const payload = JSON.parse('{"constructor": {"prototype": {"vulnerable": "yes"}}}');
43+
const target = {};
44+
45+
extend(target, payload);
46+
47+
expect({}.vulnerable).to.be.undefined;
48+
});
49+
});
50+
});

0 commit comments

Comments
 (0)