-
Notifications
You must be signed in to change notification settings - Fork 672
Security T1330839 - DevExtreme(devextreme-dist@26.1.3) - Potential Prototype Pollution (CWE-1321) #34091
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 26_1
Are you sure you want to change the base?
Security T1330839 - DevExtreme(devextreme-dist@26.1.3) - Potential Prototype Pollution (CWE-1321) #34091
Changes from all commits
565b2a2
9239cd3
bfbe0b7
fcee1db
ee4429b
36866cb
1ab7b73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,10 @@ const bracketsToDots = function (expr) { | |
| .replace(/\]/g, ''); | ||
| }; | ||
|
|
||
| const UNSAFE_PATH_FRAGMENTS = new Set(['__proto__', 'constructor', 'prototype']); | ||
|
|
||
| const isUnsafePathFragment = (name: string): boolean => UNSAFE_PATH_FRAGMENTS.has(name); | ||
|
|
||
| export const getPathParts = function (name) { | ||
| return bracketsToDots(name).split('.'); | ||
| }; | ||
|
|
@@ -64,8 +68,14 @@ export const compileGetter = function (expr) { | |
|
|
||
| if (typeof expr === 'string') { | ||
| const path = getPathParts(expr); | ||
| const unsafeFragment = path.find(isUnsafePathFragment); | ||
|
|
||
| return function (obj, options) { | ||
| if (unsafeFragment !== undefined) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's move the check into compileGetter at the compilation stage. Now it returns a separate no-op function for an unsafe path, as already done in compileSetter. The getter hot path no longer carries a per-call check, and both functions will be symmetrical. See comment below |
||
| errors.log('E0123', unsafeFragment); | ||
| return; | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| options = prepareOptions(options); | ||
| const functionAsIs = options.functionsAsIs; | ||
| const hasDefaultValue = 'defaultValue' in options; | ||
|
|
@@ -164,9 +174,17 @@ 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) { | ||
| return function () { | ||
| errors.log('E0123', unsafeFragment); | ||
| }; | ||
| } | ||
|
|
||
| return function (obj, value, options) { | ||
| options = prepareOptions(options); | ||
|
|
||
| let currentValue = unwrap(obj, options); | ||
|
|
||
| expr.forEach(function (propertyName, levelIndex) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 = { | ||
|
|
@@ -480,6 +481,102 @@ 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('compileGetter logs error and returns undefined for __proto__ path fragment (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('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'); | ||
| }); | ||
| }); | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
|
|
||
| QUnit.module('setter with wrapped variables', { | ||
| beforeEach: function() { | ||
| variableWrapper.inject(mockVariableWrapper); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please verify that your error appears on the documentation page (the technical writers' ticket/card will remain in place) and coordinate this with them.