diff --git a/packages/csp/src/parser.js b/packages/csp/src/parser.js
index 2a79bdcf1..6cf67a9bc 100644
--- a/packages/csp/src/parser.js
+++ b/packages/csp/src/parser.js
@@ -794,6 +794,10 @@ class Evaluator {
const prop = node.argument.computed
? this.evaluate({ node: node.argument.property, scope, context, forceBindingRootScopeToFunctions })
: node.argument.property.name;
+ if (this.isDOMObject(obj)) {
+ throw new Error('Property assignments on DOM objects are prohibited in the CSP build');
+ }
+ this.checkForDangerousKeywords(prop);
const oldValue = obj[prop];
if (node.operator === '++') {
@@ -843,7 +847,17 @@ class Evaluator {
scope[node.left.name] = value;
return value;
} else if (node.left.type === 'MemberExpression') {
- throw new Error('Property assignments are prohibited in the CSP build')
+ const obj = this.evaluate({ node: node.left.object, scope, context, forceBindingRootScopeToFunctions });
+ const prop = node.left.computed
+ ? this.evaluate({ node: node.left.property, scope, context, forceBindingRootScopeToFunctions })
+ : node.left.property.name;
+ if (this.isDOMObject(obj)) {
+ throw new Error('Property assignments on DOM objects are prohibited in the CSP build');
+ }
+ this.checkForDangerousKeywords(prop);
+
+ obj[prop] = value;
+ return value;
}
throw new Error('Invalid assignment target');
@@ -868,11 +882,21 @@ class Evaluator {
}
}
+ isDOMObject(obj) {
+ return obj instanceof Node
+ || (typeof CSSStyleDeclaration !== 'undefined' && obj instanceof CSSStyleDeclaration)
+ || (typeof DOMStringMap !== 'undefined' && obj instanceof DOMStringMap)
+ || (typeof DOMTokenList !== 'undefined' && obj instanceof DOMTokenList)
+ || (typeof NamedNodeMap !== 'undefined' && obj instanceof NamedNodeMap)
+ }
+
checkForDangerousKeywords(keyword) {
let blacklist = [
'constructor', 'prototype', '__proto__',
'__defineGetter__', '__defineSetter__',
'insertAdjacentHTML',
+ 'setAttribute', 'setAttributeNS',
+ 'setAttributeNode', 'setAttributeNodeNS',
]
if (blacklist.includes(keyword)) {
diff --git a/tests/cypress/integration/plugins/csp-compatibility.spec.js b/tests/cypress/integration/plugins/csp-compatibility.spec.js
index 3b1682305..109f678f1 100644
--- a/tests/cypress/integration/plugins/csp-compatibility.spec.js
+++ b/tests/cypress/integration/plugins/csp-compatibility.spec.js
@@ -15,6 +15,20 @@ test.csp('supports regular syntax',
}
)
+test.csp('supports x-model with dotted path',
+ [html`
+
+
+
+
+ `],
+ ({ get }) => {
+ get('span').should(haveText('initial'))
+ get('input').clear().type('updated')
+ get('span').should(haveText('updated'))
+ }
+)
+
test.csp('throws when accessing a global',
[html`
@@ -59,12 +73,12 @@ test.csp('throws when accessing a global via function',
},
)
-test.csp('throws when parsing a property assignment',
+test.csp('throws when assigning to a DOM node property',
[html`
`],
(cy) => {
- cy.on('uncaught:exception', ({message}) => message.includes('Property assignments are prohibited') ? false : true)
+ cy.on('uncaught:exception', ({message}) => message.includes('Property assignments on DOM objects are prohibited') ? false : true)
cy.get('button').click()
cy.get('button').should(notContain('evil'))
},
diff --git a/tests/vitest/csp-evaluator.spec.js b/tests/vitest/csp-evaluator.spec.js
index 58e18c6f0..03b5bbae6 100644
--- a/tests/vitest/csp-evaluator.spec.js
+++ b/tests/vitest/csp-evaluator.spec.js
@@ -95,3 +95,237 @@ describe('cspRawEvaluator', () => {
expect(cspRawEvaluator(element, '!false')).toBe(true)
});
});
+
+describe('MemberExpression assignments', () => {
+ it('simple dot-path assignment (x-model="form.name" setter)', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { form: { name: '' }, __placeholder: 'Alice' }
+
+ cspRawEvaluator(element, 'form.name = __placeholder', { scope })
+
+ expect(scope.form.name).toBe('Alice')
+ });
+
+ it('nested dot-path assignment', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { a: { b: { c: 0 } }, __placeholder: 42 }
+
+ cspRawEvaluator(element, 'a.b.c = __placeholder', { scope })
+
+ expect(scope.a.b.c).toBe(42)
+ });
+
+ it('computed property assignment', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { obj: {}, key: 'x', __placeholder: 1 }
+
+ cspRawEvaluator(element, 'obj[key] = __placeholder', { scope })
+
+ expect(scope.obj.x).toBe(1)
+ });
+
+ it('assignment returns the assigned value', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { form: { name: '' }, __placeholder: 'Alice' }
+
+ let result = cspRawEvaluator(element, 'form.name = __placeholder', { scope })
+
+ expect(result).toBe('Alice')
+ });
+
+ it('computed property with string literal key', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { obj: {}, __placeholder: 'hello' }
+
+ cspRawEvaluator(element, "obj['key'] = __placeholder", { scope })
+
+ expect(scope.obj.key).toBe('hello')
+ });
+
+ it('array index assignment', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { arr: ['a', 'b', 'c'], __placeholder: 'z' }
+
+ cspRawEvaluator(element, 'arr[0] = __placeholder', { scope })
+
+ expect(scope.arr[0]).toBe('z')
+ expect(scope.arr).toEqual(['z', 'b', 'c'])
+ });
+
+ it('right-hand side reads a member expression', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { form: { name: '' }, other: { value: 'Bob' } }
+
+ cspRawEvaluator(element, 'form.name = other.value', { scope })
+
+ expect(scope.form.name).toBe('Bob')
+ });
+
+ it('identifier assignment still works (regression guard)', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { name: '', __placeholder: 'Carol' }
+
+ cspRawEvaluator(element, 'name = __placeholder', { scope })
+
+ expect(scope.name).toBe('Carol')
+ });
+
+ it('dangerous keyword assignment is still blocked', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { obj: {}, __placeholder: 'evil' }
+
+ expect(() => {
+ cspRawEvaluator(element, 'obj.__proto__ = __placeholder', { scope })
+ }).toThrow('prohibited')
+
+ expect(() => {
+ cspRawEvaluator(element, 'obj.constructor = __placeholder', { scope })
+ }).toThrow('prohibited')
+
+ expect(() => {
+ cspRawEvaluator(element, 'obj.prototype = __placeholder', { scope })
+ }).toThrow('prohibited')
+ });
+
+ it('DOM node check takes precedence over dangerous keyword check', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let domNode = document.createElement('div')
+ let scope = { $el: domNode, __placeholder: 'evil' }
+
+ expect(() => {
+ cspRawEvaluator(element, '$el.__proto__ = __placeholder', { scope })
+ }).toThrow('DOM objects are prohibited')
+ });
+
+ it('DOM node update check takes precedence over dangerous keyword check', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let domNode = document.createElement('div')
+ let scope = { $el: domNode }
+
+ expect(() => {
+ cspRawEvaluator(element, '$el.__proto__++', { scope })
+ }).toThrow('DOM objects are prohibited')
+ });
+
+ it('postfix increment on scope object', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { obj: { count: 0 } }
+
+ let result = cspRawEvaluator(element, 'obj.count++', { scope })
+
+ expect(scope.obj.count).toBe(1)
+ expect(result).toBe(0)
+ });
+
+ it('prefix increment on scope object', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { obj: { count: 0 } }
+
+ let result = cspRawEvaluator(element, '++obj.count', { scope })
+
+ expect(scope.obj.count).toBe(1)
+ expect(result).toBe(1)
+ });
+
+ it('decrement on scope object', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { obj: { count: 5 } }
+
+ cspRawEvaluator(element, 'obj.count--', { scope })
+
+ expect(scope.obj.count).toBe(4)
+ });
+
+ it('computed property update expression', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { obj: { x: 10 }, key: 'x' }
+
+ cspRawEvaluator(element, 'obj[key]++', { scope })
+
+ expect(scope.obj.x).toBe(11)
+ });
+
+ it('dangerous keyword update expression is blocked on scope object', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let scope = { obj: {} }
+
+ expect(() => {
+ cspRawEvaluator(element, 'obj.__proto__++', { scope })
+ }).toThrow('prohibited')
+ });
+
+ it('DOM node property assignment is blocked', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let domNode = document.createElement('div')
+ let scope = { $el: domNode, __placeholder: 'evil' }
+
+ expect(() => {
+ cspRawEvaluator(element, '$el.innerHTML = __placeholder', { scope })
+ }).toThrow('DOM objects are prohibited')
+
+ expect(() => {
+ cspRawEvaluator(element, '$el.textContent = __placeholder', { scope })
+ }).toThrow('DOM objects are prohibited')
+ });
+
+ it('DOM node update expression is blocked', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let domNode = document.createElement('div')
+ domNode.count = 5
+ let scope = { $el: domNode }
+
+ expect(() => {
+ cspRawEvaluator(element, '$el.count++', { scope })
+ }).toThrow('DOM objects are prohibited')
+ });
+
+ it('setAttribute is blocked via keyword blocklist', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let domNode = document.createElement('div')
+ let scope = { $el: domNode }
+
+ expect(() => {
+ cspRawEvaluator(element, '$el.setAttribute("onclick", "alert(1)")', { scope })
+ }).toThrow('prohibited')
+ });
+
+ it('setAttributeNS is blocked via keyword blocklist', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let domNode = document.createElement('div')
+ let scope = { $el: domNode }
+
+ expect(() => {
+ cspRawEvaluator(element, '$el.setAttributeNS(null, "onclick", "alert(1)")', { scope })
+ }).toThrow('prohibited')
+ });
+
+ it('setAttribute via computed property is blocked', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let domNode = document.createElement('div')
+ let scope = { $el: domNode, method: 'setAttribute' }
+
+ expect(() => {
+ cspRawEvaluator(element, '$el[method]("onclick", "alert(1)")', { scope })
+ }).toThrow('prohibited')
+ });
+
+ it('CSSStyleDeclaration assignment is blocked', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let domNode = document.createElement('div')
+ let scope = { style: domNode.style, __placeholder: 'red' }
+
+ expect(() => {
+ cspRawEvaluator(element, 'style.background = __placeholder', { scope })
+ }).toThrow('DOM objects are prohibited')
+ });
+
+ it('DOMStringMap assignment is blocked', () => {
+ let element = { parentNode: null, _x_dataStack: [] }
+ let domNode = document.createElement('div')
+ let scope = { dataset: domNode.dataset, __placeholder: 'evil' }
+
+ expect(() => {
+ cspRawEvaluator(element, 'dataset.key = __placeholder', { scope })
+ }).toThrow('DOM objects are prohibited')
+ });
+});
diff --git a/tests/vitest/csp-parser.spec.js b/tests/vitest/csp-parser.spec.js
index 423600ae5..fb8e0f69a 100644
--- a/tests/vitest/csp-parser.spec.js
+++ b/tests/vitest/csp-parser.spec.js
@@ -597,12 +597,37 @@ describe('CSP Parser', () => {
expect(() => generateRuntimeFunction('JSON.stringify({a: 1})')()).toThrow();
});
- it('should not handle property assignment', () => {
- expect(() => generateRuntimeFunction('obj.prop = 10')()).toThrow();
+ it('should handle scope property assignment', () => {
+ const scope = { obj: { prop: 0 } };
+ generateRuntimeFunction('obj.prop = 10')({ scope });
+ expect(scope.obj.prop).toBe(10);
});
- it('should not handle computed property assignment', () => {
- expect(() => generateRuntimeFunction('obj[key] = 20')()).toThrow();
+ it('should handle computed scope property assignment', () => {
+ const scope = { obj: {}, key: 'x' };
+ generateRuntimeFunction('obj[key] = 20')({ scope });
+ expect(scope.obj.x).toBe(20);
+ });
+
+ it('should block DOM node property assignment', () => {
+ const scope = { $el: document.createElement('div') };
+ expect(() => generateRuntimeFunction('$el.innerHTML = "evil"')({ scope })).toThrow('DOM objects are prohibited');
+ });
+
+ it('should block DOM node computed property assignment', () => {
+ const scope = { $el: document.createElement('div'), key: 'innerHTML' };
+ expect(() => generateRuntimeFunction('$el[key] = "evil"')({ scope })).toThrow('DOM objects are prohibited');
+ });
+
+ it('should block setAttribute on DOM nodes', () => {
+ const scope = { $el: document.createElement('div') };
+ expect(() => generateRuntimeFunction('$el.setAttribute("onclick", "alert(1)")')({ scope })).toThrow('prohibited');
+ });
+
+ it('should block CSSStyleDeclaration assignment', () => {
+ const el = document.createElement('div');
+ const scope = { style: el.style };
+ expect(() => generateRuntimeFunction('style.background = "red"')({ scope })).toThrow('DOM objects are prohibited');
});
});