Skip to content

Commit 565b2a2

Browse files
committed
prevent prototype pollution via unsafe path
1 parent 66e1317 commit 565b2a2

3 files changed

Lines changed: 110 additions & 1 deletion

File tree

packages/devextreme/js/__internal/core/m_errors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ export default errorUtils({
6464

6565
E0122: 'AIIntegration: The sendRequest method is missing.',
6666

67+
E0123: 'Prototype pollution attempt detected: the path contains an unsafe fragment \'{0}\'',
68+
6769
W0000: '\'{0}\' is deprecated in {1}. {2}',
6870

6971
W0001: '{0} - \'{1}\' option is deprecated in {2}. {3}',

packages/devextreme/js/__internal/core/utils/m_data.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ const bracketsToDots = function (expr) {
1717
.replace(/\]/g, '');
1818
};
1919

20+
const UNSAFE_PATH_FRAGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
21+
22+
const isUnsafePathFragment = (name: string): boolean => UNSAFE_PATH_FRAGMENTS.has(name);
23+
2024
export const getPathParts = function (name) {
2125
return bracketsToDots(name).split('.');
2226
};
@@ -81,6 +85,11 @@ export const compileGetter = function (expr) {
8185

8286
const pathPart = path[i];
8387

88+
if (isUnsafePathFragment(pathPart)) {
89+
errors.log('E0123', pathPart);
90+
return;
91+
}
92+
8493
if (hasDefaultValue && isObject(current) && !(pathPart in current)) {
8594
return options.defaultValue;
8695
}
@@ -130,13 +139,23 @@ function combineGetters(getters) {
130139

131140
for (let i = 0; i < last; i++) {
132141
const pathItem = path[i];
142+
if (isUnsafePathFragment(pathItem)) {
143+
errors.log('E0123', pathItem);
144+
return;
145+
}
133146
if (!(pathItem in current)) {
134147
current[pathItem] = { };
135148
}
136149
current = current[pathItem];
137150
}
138151

139-
current[path[last]] = value;
152+
const lastPathItem = path[last];
153+
if (isUnsafePathFragment(lastPathItem)) {
154+
errors.log('E0123', lastPathItem);
155+
return;
156+
}
157+
158+
current[lastPathItem] = value;
140159
});
141160
return result;
142161
};
@@ -170,6 +189,11 @@ export const compileSetter = function (expr) {
170189
let currentValue = unwrap(obj, options);
171190

172191
expr.forEach(function (propertyName, levelIndex) {
192+
if (isUnsafePathFragment(propertyName)) {
193+
errors.log('E0123', propertyName);
194+
return;
195+
}
196+
173197
let propertyValue = readPropValue(currentValue, propertyName, options);
174198
const isPropertyFunc = !options.functionsAsIs && isFunction(propertyValue) && !isWrapped(propertyValue);
175199

packages/devextreme/testing/tests/DevExpress.core/utils.data.tests.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
compileGetter as GETTER,
33
compileSetter as SETTER
44
} from 'core/utils/data';
5+
import errors from 'core/errors';
56
import variableWrapper from 'core/utils/variable_wrapper';
67

78
const mockVariableWrapper = {
@@ -480,6 +481,88 @@ QUnit.module('setter', () => {
480481
});
481482

482483

484+
QUnit.module('prototype pollution protection', {
485+
beforeEach: function() {
486+
sinon.spy(errors, 'log');
487+
},
488+
afterEach: function() {
489+
errors.log.restore();
490+
delete Object.prototype['pp_dx'];
491+
delete Object.prototype['pp_dx2'];
492+
}
493+
}, () => {
494+
495+
test('compileSetter logs error and skips assignment for __proto__ path fragment', function(assert) {
496+
SETTER('__proto__.pp_dx')({}, 'yes', { functionsAsIs: true });
497+
498+
assert.strictEqual(errors.log.calledWith('E0123', '__proto__'), true, 'should log E0123 for __proto__');
499+
assert.strictEqual(({}).pp_dx, undefined, 'Object.prototype must not be polluted');
500+
});
501+
502+
test('compileSetter logs error and skips assignment for constructor path fragment', function(assert) {
503+
SETTER('constructor.prototype.pp_dx')({}, 'yes', { functionsAsIs: true });
504+
505+
assert.strictEqual(errors.log.calledWith('E0123', 'constructor'), true, 'should log E0123 for constructor');
506+
assert.strictEqual(({}).pp_dx, undefined, 'Object.prototype must not be polluted');
507+
});
508+
509+
test('compileSetter logs error and skips assignment for prototype path fragment', function(assert) {
510+
const fn = function() {};
511+
SETTER('prototype.pp_dx')(fn, 'yes', { functionsAsIs: true });
512+
513+
assert.strictEqual(errors.log.calledWith('E0123', 'prototype'), true, 'should log E0123 for prototype');
514+
assert.strictEqual(fn.prototype.pp_dx, undefined, 'function prototype must not be modified');
515+
});
516+
517+
test('compileSetter works normally for safe paths', function(assert) {
518+
const obj = { a: { b: 1 } };
519+
SETTER('a.b')(obj, 42);
520+
521+
assert.strictEqual(obj.a.b, 42, 'safe paths must still work');
522+
assert.strictEqual(errors.log.called, false, 'should not log for safe paths');
523+
});
524+
525+
test('compileGetter logs error and returns undefined for __proto__ path fragment', function(assert) {
526+
const result = GETTER('__proto__.pp_dx')({});
527+
528+
assert.strictEqual(errors.log.calledWith('E0123', '__proto__'), true, 'should log E0123 for __proto__');
529+
assert.strictEqual(result, undefined, 'getter must return undefined for __proto__');
530+
});
531+
532+
test('compileGetter logs error and returns undefined for constructor path fragment', function(assert) {
533+
const result = GETTER('constructor.prototype')(function() {});
534+
535+
assert.strictEqual(errors.log.calledWith('E0123', 'constructor'), true, 'should log E0123 for constructor');
536+
assert.strictEqual(result, undefined, 'getter must return undefined for constructor');
537+
});
538+
539+
test('compileGetter logs error and returns undefined for prototype path fragment', function(assert) {
540+
const result = GETTER('prototype.pp_dx')(function() {});
541+
542+
assert.strictEqual(errors.log.calledWith('E0123', 'prototype'), true, 'should log E0123 for prototype');
543+
assert.strictEqual(result, undefined, 'getter must return undefined for prototype');
544+
});
545+
546+
test('combineGetters logs error and skips __proto__ fragment, returns safe fields', function(assert) {
547+
const obj = { safe: 'value' };
548+
const result = GETTER(['__proto__.pp_dx', 'safe'])(obj);
549+
550+
assert.strictEqual(errors.log.calledWith('E0123', '__proto__'), true, 'should log E0123 for __proto__');
551+
assert.strictEqual(({}).pp_dx, undefined, 'Object.prototype must not be polluted');
552+
assert.deepEqual(result, { safe: 'value' }, 'safe field must still be returned');
553+
});
554+
555+
test('combineGetters logs error and skips constructor fragment, returns safe fields', function(assert) {
556+
const obj = { safe: 'value' };
557+
const result = GETTER(['constructor.prototype.pp_dx', 'safe'])(obj);
558+
559+
assert.strictEqual(errors.log.calledWith('E0123', 'constructor'), true, 'should log E0123 for constructor');
560+
assert.strictEqual(({}).pp_dx, undefined, 'Object.prototype must not be polluted');
561+
assert.deepEqual(result, { safe: 'value' }, 'safe field must still be returned');
562+
});
563+
});
564+
565+
483566
QUnit.module('setter with wrapped variables', {
484567
beforeEach: function() {
485568
variableWrapper.inject(mockVariableWrapper);

0 commit comments

Comments
 (0)