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
26 changes: 25 additions & 1 deletion packages/csp/src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 === '++') {
Expand Down Expand Up @@ -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');

Expand All @@ -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)) {
Expand Down
18 changes: 16 additions & 2 deletions tests/cypress/integration/plugins/csp-compatibility.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ test.csp('supports regular syntax',
}
)

test.csp('supports x-model with dotted path',
[html`
<div x-data="{ form: { name: 'initial' } }">
<input x-model="form.name" />
<span x-text="form.name"></span>
</div>
`],
({ get }) => {
get('span').should(haveText('initial'))
get('input').clear().type('updated')
get('span').should(haveText('updated'))
}
)

test.csp('throws when accessing a global',
[html`
<button x-data x-on:click="document.write('evil')"></button>
Expand Down Expand Up @@ -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`
<button x-data x-on:click="$el.innerHTML = 'evil'"></button>
`],
(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'))
},
Expand Down
234 changes: 234 additions & 0 deletions tests/vitest/csp-evaluator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
});
});
33 changes: 29 additions & 4 deletions tests/vitest/csp-parser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});

Expand Down
Loading