Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/devextreme/js/__internal/core/m_errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export default errorUtils({

E0121: 'The \'customItem\' parameter of the \'onCustomItemCreating\' function is empty or contains invalid data. Assign a custom object or a Promise that is resolved after the item is created.',

E0123: 'Disallowed property name in expression: \'{0}\'',

W0000: '\'{0}\' is deprecated in {1}. {2}',

W0001: '{0} - \'{1}\' option is deprecated in {2}. {3}',
Expand Down
40 changes: 40 additions & 0 deletions packages/devextreme/js/__internal/core/utils/m_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ const bracketsToDots = function (expr) {
.replace(/\]/g, '');
};

const UNSAFE_PATH_FRAGMENTS = new Set(['__proto__', 'constructor']);

const isUnsafePathFragment = (name: string): boolean => UNSAFE_PATH_FRAGMENTS.has(name);
const isUnsafePrototypeAccess = (obj: any, name: string): boolean => name === 'prototype' && typeof obj === 'function';

export const getPathParts = function (name) {
return bracketsToDots(name).split('.');
};
Expand Down Expand Up @@ -64,6 +69,15 @@ export const compileGetter = function (expr) {

if (typeof expr === 'string') {
const path = getPathParts(expr);
const unsafeFragment = path.find(isUnsafePathFragment);

if (unsafeFragment !== undefined) {
errors.log('E0123', unsafeFragment);
return function () {
// eslint-disable-next-line no-useless-return
return;
};
}

return function (obj, options) {
options = prepareOptions(options);
Expand All @@ -72,6 +86,11 @@ export const compileGetter = function (expr) {
let current = unwrap(obj, options);

for (let i = 0; i < path.length; i++) {
if (isUnsafePrototypeAccess(current, path[i])) {
errors.log('E0123', path[i]);
return;
}

if (!current) {
if (current == null && hasDefaultValue) {
return options.defaultValue;
Expand Down Expand Up @@ -164,12 +183,33 @@ const ensurePropValueDefined = function (obj, propName, value, options) {
export const compileSetter = function (expr) {
expr = getPathParts(expr || 'this');
const lastLevelIndex = expr.length - 1;
const unsafeFragment = expr.find(isUnsafePathFragment);

if (unsafeFragment !== undefined) {
errors.log('E0123', unsafeFragment);
return function () {
// eslint-disable-next-line no-useless-return
return;
};
}

return function (obj, value, options) {
options = prepareOptions(options);

let currentValue = unwrap(obj, options);
let hasUnsafePrototypeAccess = false;

expr.forEach(function (propertyName, levelIndex) {
if (hasUnsafePrototypeAccess) {
return;
}

if (isUnsafePrototypeAccess(currentValue, propertyName)) {
errors.log('E0123', propertyName);
hasUnsafePrototypeAccess = true;
return;
}

let propertyValue = readPropValue(currentValue, propertyName, options);
const isPropertyFunc = !options.functionsAsIs && isFunction(propertyValue) && !isWrapped(propertyValue);

Expand Down
130 changes: 130 additions & 0 deletions packages/devextreme/testing/tests/DevExpress.core/utils.data.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
compileGetter as GETTER,
compileSetter as SETTER
} from 'core/utils/data';
import errors from 'core/errors';
import variableWrapper from 'core/utils/variable_wrapper';

const mockVariableWrapper = {
Expand Down Expand Up @@ -480,6 +481,135 @@ QUnit.module('setter', () => {
});


QUnit.module('prototype pollution protection', {
beforeEach: function() {
sinon.spy(errors, 'log');
},
afterEach: function() {
errors.log.restore();
delete Object.prototype['pp_dx'];
delete Object.prototype['pp_dx2'];
}
}, () => {

test('compileSetter logs error and skips assignment for __proto__ path fragment (T1330839)', function(assert) {
const obj = {};
SETTER('__proto__.pp_dx')(obj, 'yes', { functionsAsIs: true });

assert.strictEqual(errors.log.calledWith('E0123', '__proto__'), true, 'should log E0123 for __proto__');
assert.strictEqual(obj.pp_dx, undefined, 'target object must not be modified');
assert.strictEqual(({}).pp_dx, undefined, 'Object.prototype must not be polluted');
});

test('compileSetter logs error and skips assignment for constructor path fragment (T1330839)', function(assert) {
const obj = {};
SETTER('constructor.prototype.pp_dx')(obj, 'yes', { functionsAsIs: true });

assert.strictEqual(errors.log.calledWith('E0123', 'constructor'), true, 'should log E0123 for constructor');
assert.strictEqual(obj.pp_dx, undefined, 'target object must not be modified');
assert.strictEqual(({}).pp_dx, undefined, 'Object.prototype must not be polluted');
});

test('compileSetter logs error and skips assignment for prototype path fragment (T1330839)', function(assert) {
const fn = function() {};
SETTER('prototype.pp_dx')(fn, 'yes', { functionsAsIs: true });

assert.strictEqual(errors.log.calledWith('E0123', 'prototype'), true, 'should log E0123 for prototype');
assert.strictEqual(fn.pp_dx, undefined, 'function object must not be modified');
assert.strictEqual(fn.prototype.pp_dx, undefined, 'function prototype must not be modified');
});

test('compileSetter works normally for safe paths (T1330839)', function(assert) {
const obj = { a: { b: 1 } };
SETTER('a.b')(obj, 42);

assert.strictEqual(obj.a.b, 42, 'safe paths must still work');
assert.strictEqual(errors.log.called, false, 'should not log for safe paths');
});

test('compileSetter allows prototype property on plain objects (T1330839)', function(assert) {
const obj = {};
SETTER('prototype')(obj, 'yes');

assert.strictEqual(obj.prototype, 'yes', 'should allow setting prototype on plain objects');
assert.strictEqual(errors.log.called, false, 'should not log error for plain objects');
});

test('compileSetter blocks unsafe fragment written in bracket notation (T1330839)', function(assert) {
const obj = {};
SETTER('a[constructor][prototype].pp_dx')(obj, 'yes', { functionsAsIs: true });

assert.strictEqual(errors.log.calledWith('E0123', 'constructor'), true, 'should log E0123 for constructor in bracket notation');
assert.strictEqual(({}).pp_dx, undefined, 'Object.prototype must not be polluted');
});

test('compileSetter blocks unsafe fragment in non-first position (T1330839)', function(assert) {
const obj = { a: { b: {} } };
SETTER('a.b.__proto__')(obj, { pp_dx: 'yes' }, { functionsAsIs: true });

assert.strictEqual(errors.log.calledWith('E0123', '__proto__'), true, 'should log E0123 for __proto__ in non-first position');
assert.strictEqual(obj.a.b.pp_dx, undefined, 'nested object must not inherit a polluted property');
assert.strictEqual(({}).pp_dx, undefined, 'Object.prototype must not be polluted');
});

test('compileGetter logs error and returns undefined for __proto__ path fragent (T1330839)', function(assert) {
const result = GETTER('__proto__.pp_dx')({});

assert.strictEqual(errors.log.calledWith('E0123', '__proto__'), true, 'should log E0123 for __proto__');
assert.strictEqual(result, undefined, 'getter must return undefined for __proto__');
});

test('compileGetter logs error and returns undefined for constructor path fragment (T1330839)', function(assert) {
const result = GETTER('constructor.prototype')(function() {});

assert.strictEqual(errors.log.calledWith('E0123', 'constructor'), true, 'should log E0123 for constructor');
assert.strictEqual(result, undefined, 'getter must return undefined for constructor');
});

test('compileGetter logs error and returns undefined for prototype path fragment (T1330839)', function(assert) {
const result = GETTER('prototype.pp_dx')(function() {});

assert.strictEqual(errors.log.calledWith('E0123', 'prototype'), true, 'should log E0123 for prototype');
assert.strictEqual(result, undefined, 'getter must return undefined for prototype');
});

test('compileGetter allows prototype property on plain objects (T1330839)', function(assert) {
const obj = { prototype: 'yes' };
const result = GETTER('prototype')(obj);

assert.strictEqual(result, 'yes', 'should allow getting prototype on plain objects');
assert.strictEqual(errors.log.called, false, 'should not log error for plain objects');
});

test('combineGetters logs error and skips __proto__ fragment, returns safe fields (T1330839)', function(assert) {
const obj = { safe: 'value' };
const result = GETTER(['__proto__.pp_dx', 'safe'])(obj);

assert.strictEqual(errors.log.calledWith('E0123', '__proto__'), true, 'should log E0123 for __proto__');
assert.strictEqual(({}).pp_dx, undefined, 'Object.prototype must not be polluted');
assert.deepEqual(result, { safe: 'value' }, 'safe field must still be returned');
});

test('combineGetters logs error and skips constructor fragment, returns safe fields (T1330839)', function(assert) {
const obj = { safe: 'value' };
const result = GETTER(['constructor.prototype.pp_dx', 'safe'])(obj);

assert.strictEqual(errors.log.calledWith('E0123', 'constructor'), true, 'should log E0123 for constructor');
assert.strictEqual(({}).pp_dx, undefined, 'Object.prototype must not be polluted');
assert.deepEqual(result, { safe: 'value' }, 'safe field must still be returned');
});

test('combineGetters logs error and skips unsafe paths even if intermediate values break early (with defaultValue) (T1330839)', function(assert) {
const obj = { a: null, safe: 'value' };
const result = GETTER(['a.constructor.prototype.pp_dx2', 'safe'])(obj, { defaultValue: 'default' });

assert.strictEqual(errors.log.calledWith('E0123', 'constructor'), true, 'should log E0123 for constructor');
assert.strictEqual(({}).pp_dx2, undefined, 'Object.prototype must not be polluted');
assert.deepEqual(result, { safe: 'value' }, 'safe field must still be returned');
});
});


QUnit.module('setter with wrapped variables', {
beforeEach: function() {
variableWrapper.inject(mockVariableWrapper);
Expand Down
Loading