Skip to content

Commit 9977ece

Browse files
committed
Improve robustness against sensitive properties in core extend and markup parser. Restored deep merge behavior in markup component and added Debug.assert for reporting.
1 parent 0afe3a6 commit 9977ece

3 files changed

Lines changed: 98 additions & 6 deletions

File tree

src/core/core.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,23 @@ const version = '$_CURRENT_SDK_VERSION';
99
*/
1010
const revision = '$_CURRENT_SDK_REVISION';
1111

12+
import { Debug } from './debug.js';
13+
1214
/**
1315
* Merge the contents of two objects into a single object.
1416
*
1517
* @param {object} target - The target object of the merge.
16-
* @param {object} ex - The object that is merged with target.
18+
* @param {object} ex - The object to be merged into the target.
1719
* @returns {object} The target object.
1820
* @example
19-
* const A = {
21+
* var A = {
2022
* a: function () {
21-
* console.log(this.a);
23+
* console.log('a');
2224
* }
2325
* };
24-
* const B = {
26+
* var B = {
2527
* b: function () {
26-
* console.log(this.b);
28+
* console.log('b');
2729
* }
2830
* };
2931
*
@@ -36,6 +38,16 @@ const revision = '$_CURRENT_SDK_REVISION';
3638
*/
3739
function extend(target, ex) {
3840
for (const prop in ex) {
41+
if (!Object.prototype.hasOwnProperty.call(ex, prop)) {
42+
continue;
43+
}
44+
45+
const isForbidden = prop === '__proto__' || prop === 'constructor' || prop === 'prototype';
46+
Debug.assert(!isForbidden, `Ignoring forbidden property: ${prop}`);
47+
if (isForbidden) {
48+
continue;
49+
}
50+
3951
const copy = ex[prop];
4052

4153
if (Array.isArray(copy)) {
@@ -50,4 +62,5 @@ function extend(target, ex) {
5062
return target;
5163
}
5264

65+
5366
export { extend, revision, version };

src/framework/components/element/markup.js

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

35
// list of scanner tokens
@@ -334,9 +336,16 @@ class Parser {
334336
// of assign)
335337
function merge(target, source) {
336338
for (const key in source) {
337-
if (!source.hasOwnProperty(key)) {
339+
if (!Object.prototype.hasOwnProperty.call(source, key)) {
338340
continue;
339341
}
342+
343+
const isForbidden = key === '__proto__' || key === 'constructor' || key === 'prototype';
344+
Debug.assert(!isForbidden, `Ignoring forbidden property: ${key}`);
345+
if (isForbidden) {
346+
continue;
347+
}
348+
340349
const value = source[key];
341350
if (value instanceof Object) {
342351
if (!target.hasOwnProperty(key)) {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
// [__proto__ polluted="true"]hello[/__proto__]
9+
const symbols = [
10+
'[', '_', '_', 'p', 'r', 'o', 't', 'o', '_', '_', ' ', 'p', 'o', 'l', 'l', 'u', 't', 'e', 'd', '=', '"', 't', 'r', 'u', 'e', '"', ']',
11+
'h', 'e', 'l', 'l', 'o',
12+
'[', '/', '_', '_', 'p', 'r', 'o', 't', 'o', '_', '_', ']'
13+
];
14+
15+
// Ensure Object.prototype is clean before test
16+
expect({}.polluted).to.be.undefined;
17+
18+
const results = Markup.evaluate(symbols);
19+
20+
expect(results.tags).to.not.equal(null);
21+
results.tags.forEach((tag) => {
22+
if (tag) {
23+
expect(Object.prototype.hasOwnProperty.call(tag, '__proto__')).to.be.false;
24+
}
25+
});
26+
27+
expect({}.polluted).to.be.undefined;
28+
});
29+
30+
it('should ignore constructor and prototype keys', function () {
31+
// [constructor]test[/constructor]
32+
const symbols = [
33+
'[', 'c', 'o', 'n', 's', 't', 'r', 'u', 'c', 't', 'o', 'r', ']',
34+
't', 'e', 's', 't',
35+
'[', '/', 'c', 'o', 'n', 's', 't', 'r', 'u', 'c', 't', 'o', 'r', ']'
36+
];
37+
38+
const results = Markup.evaluate(symbols);
39+
expect(results.tags).to.not.equal(null);
40+
results.tags.forEach((tag) => {
41+
if (tag) {
42+
expect(Object.prototype.hasOwnProperty.call(tag, 'constructor')).to.be.false;
43+
}
44+
});
45+
});
46+
});
47+
48+
describe('Core: extend utility', function () {
49+
it('should protect against prototype pollution', function () {
50+
const payload = JSON.parse('{"__proto__": {"vulnerable": "yes"}}');
51+
const target = {};
52+
53+
const originalPrototype = Object.getPrototypeOf(target);
54+
extend(target, payload);
55+
56+
expect({}.vulnerable).to.be.undefined;
57+
expect(target.vulnerable).to.be.undefined;
58+
expect(Object.getPrototypeOf(target)).to.equal(originalPrototype);
59+
});
60+
61+
it('should protect against constructor/prototype pollution', function () {
62+
const payload = JSON.parse('{"constructor": {"prototype": {"vulnerable": "yes"}}}');
63+
const target = {};
64+
65+
extend(target, payload);
66+
67+
expect({}.vulnerable).to.be.undefined;
68+
});
69+
});
70+
});

0 commit comments

Comments
 (0)