WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 56b0759

Browse files
calebporzioganyicz
andauthored
Imrove CSP bundle (#4704)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip --------- Co-authored-by: Filip Ganyicz <[email protected]>
1 parent 4ea3d71 commit 56b0759

File tree

7 files changed

+391
-155
lines changed

7 files changed

+391
-155
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { directive } from 'alpinejs/src/directives'
2+
import { handleError } from 'alpinejs/src/utils/error'
3+
4+
directive('html', (el, { expression }) => {
5+
handleError(new Error('Using the x-html directive is prohibited in the CSP build'), el)
6+
})

packages/csp/src/evaluator.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,21 @@ function generateDataStack(el) {
2626
}
2727

2828
function generateEvaluator(el, expression, dataStack) {
29+
if (el instanceof HTMLIFrameElement) {
30+
throw new Error('Evaluating expressions on an iframe is prohibited in the CSP build')
31+
}
32+
33+
if (el instanceof HTMLScriptElement) {
34+
throw new Error('Evaluating expressions on a script is prohibited in the CSP build')
35+
}
36+
2937
return (receiver = () => {}, { scope = {}, params = [] } = {}) => {
3038
let completeScope = mergeProxies([scope, ...dataStack])
3139

3240
let evaluate = generateRuntimeFunction(expression)
3341

3442
let returnValue = evaluate({
3543
scope: completeScope,
36-
allowGlobal: false,
3744
forceBindingRootScopeToFunctions: true,
3845
})
3946

packages/csp/src/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ import { reactive, effect, stop, toRaw } from '@vue/reactivity'
3131
Alpine.setReactivityEngine({ reactive, effect, release: stop, raw: toRaw })
3232

3333
import 'alpinejs/src/magics/index'
34-
3534
import 'alpinejs/src/directives/index'
3635

36+
/**
37+
* The `x-html` directive needs to be disabled here
38+
* because it is not CSP friendly. To disable it,
39+
* we'll override it with noop implementation.
40+
*/
41+
import './directives/x-html'
42+
3743
export default Alpine

packages/csp/src/parser.js

Lines changed: 98 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
let safemap = new WeakMap()
2+
let globals = new Set()
3+
4+
Object.getOwnPropertyNames(globalThis).forEach(key => {
5+
// Prevent Safari deprecation warning...
6+
if (key === 'styleMedia') return
7+
8+
globals.add(globalThis[key])
9+
})
10+
111
class Token {
212
constructor(type, value, start, end) {
313
this.type = type;
@@ -642,47 +652,47 @@ class Parser {
642652
}
643653

644654
class Evaluator {
645-
evaluate({ node, scope = {}, context = null, allowGlobal = false, forceBindingRootScopeToFunctions = true }) {
655+
evaluate({ node, scope = {}, context = null, forceBindingRootScopeToFunctions = true }) {
646656
switch (node.type) {
647657
case 'Literal':
648658
return node.value;
649659

650660
case 'Identifier':
651661
if (node.name in scope) {
652662
const value = scope[node.name];
663+
664+
this.checkForDangerousValues(value)
665+
653666
// If it's a function and we're accessing it directly (not calling it),
654667
// bind it to scope to preserve 'this' context for later calls
655668
if (typeof value === 'function') {
656669
return value.bind(scope);
657670
}
658-
return value;
659-
}
660671

661-
// Fallback to globals - let CSP catch dangerous ones at runtime
662-
if (allowGlobal && typeof globalThis[node.name] !== 'undefined') {
663-
const value = globalThis[node.name];
664-
if (typeof value === 'function') {
665-
return value.bind(globalThis);
666-
}
667672
return value;
668673
}
669674

670675
throw new Error(`Undefined variable: ${node.name}`);
671676

672677
case 'MemberExpression':
673-
const object = this.evaluate({ node: node.object, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
678+
const object = this.evaluate({ node: node.object, scope, context, forceBindingRootScopeToFunctions });
674679
if (object == null) {
675680
throw new Error('Cannot read property of null or undefined');
676681
}
677682

678-
let memberValue;
683+
let property;
679684
if (node.computed) {
680-
const property = this.evaluate({ node: node.property, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
681-
memberValue = object[property];
685+
property = this.evaluate({ node: node.property, scope, context, forceBindingRootScopeToFunctions });
682686
} else {
683-
memberValue = object[node.property.name];
687+
property = node.property.name;
684688
}
685689

690+
this.checkForDangerousKeywords(property)
691+
692+
let memberValue = object[property];
693+
694+
this.checkForDangerousValues(memberValue)
695+
686696
// If the accessed value is a function, bind it based on forceBindingRootScopeToFunctions flag
687697
if (typeof memberValue === 'function') {
688698
if (forceBindingRootScopeToFunctions) {
@@ -695,34 +705,38 @@ class Evaluator {
695705
return memberValue;
696706

697707
case 'CallExpression':
698-
const args = node.arguments.map(arg => this.evaluate({ node: arg, scope, context, allowGlobal, forceBindingRootScopeToFunctions }));
708+
const args = node.arguments.map(arg => this.evaluate({ node: arg, scope, context, forceBindingRootScopeToFunctions }));
709+
710+
let returnValue;
699711

700712
if (node.callee.type === 'MemberExpression') {
701713
// For member expressions, get the object and function separately to preserve context
702-
const obj = this.evaluate({ node: node.callee.object, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
703-
let func;
714+
const obj = this.evaluate({ node: node.callee.object, scope, context, forceBindingRootScopeToFunctions });
715+
716+
let prop;
704717
if (node.callee.computed) {
705-
const prop = this.evaluate({ node: node.callee.property, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
706-
func = obj[prop];
718+
prop = this.evaluate({ node: node.callee.property, scope, context, forceBindingRootScopeToFunctions });
707719
} else {
708-
func = obj[node.callee.property.name];
720+
prop = node.callee.property.name
709721
}
710722

723+
this.checkForDangerousKeywords(prop)
724+
725+
let func = obj[prop];
711726
if (typeof func !== 'function') {
712727
throw new Error('Value is not a function');
713728
}
714729

715730
// For member expressions, always use the object as the 'this' context
716-
return func.apply(obj, args);
731+
returnValue = func.apply(obj, args);
717732
} else {
718733
// For direct function calls (identifiers), get the original function and apply context
719734
if (node.callee.type === 'Identifier') {
720735
const name = node.callee.name;
736+
721737
let func;
722738
if (name in scope) {
723739
func = scope[name];
724-
} else if (allowGlobal && typeof globalThis[name] !== 'undefined') {
725-
func = globalThis[name];
726740
} else {
727741
throw new Error(`Undefined variable: ${name}`);
728742
}
@@ -733,21 +747,25 @@ class Evaluator {
733747

734748
// For direct calls, use provided context or the scope
735749
const thisContext = context !== null ? context : scope;
736-
return func.apply(thisContext, args);
750+
returnValue = func.apply(thisContext, args);
737751
} else {
738752
// For other expressions
739-
const callee = this.evaluate({ node: node.callee, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
753+
const callee = this.evaluate({ node: node.callee, scope, context, forceBindingRootScopeToFunctions });
740754
if (typeof callee !== 'function') {
741755
throw new Error('Value is not a function');
742756
}
743757

744758
// For other expressions, use provided context
745-
return callee.apply(context, args);
759+
returnValue = callee.apply(context, args);
746760
}
747761
}
748762

763+
this.checkForDangerousValues(returnValue)
764+
765+
return returnValue
766+
749767
case 'UnaryExpression':
750-
const argument = this.evaluate({ node: node.argument, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
768+
const argument = this.evaluate({ node: node.argument, scope, context, forceBindingRootScopeToFunctions });
751769
switch (node.operator) {
752770
case '!': return !argument;
753771
case '-': return -argument;
@@ -772,9 +790,9 @@ class Evaluator {
772790

773791
return node.prefix ? scope[name] : oldValue;
774792
} else if (node.argument.type === 'MemberExpression') {
775-
const obj = this.evaluate({ node: node.argument.object, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
793+
const obj = this.evaluate({ node: node.argument.object, scope, context, forceBindingRootScopeToFunctions });
776794
const prop = node.argument.computed
777-
? this.evaluate({ node: node.argument.property, scope, context, allowGlobal, forceBindingRootScopeToFunctions })
795+
? this.evaluate({ node: node.argument.property, scope, context, forceBindingRootScopeToFunctions })
778796
: node.argument.property.name;
779797

780798
const oldValue = obj[prop];
@@ -789,8 +807,8 @@ class Evaluator {
789807
throw new Error('Invalid update expression target');
790808

791809
case 'BinaryExpression':
792-
const left = this.evaluate({ node: node.left, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
793-
const right = this.evaluate({ node: node.right, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
810+
const left = this.evaluate({ node: node.left, scope, context, forceBindingRootScopeToFunctions });
811+
const right = this.evaluate({ node: node.right, scope, context, forceBindingRootScopeToFunctions });
794812

795813
switch (node.operator) {
796814
case '+': return left + right;
@@ -813,41 +831,34 @@ class Evaluator {
813831
}
814832

815833
case 'ConditionalExpression':
816-
const test = this.evaluate({ node: node.test, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
834+
const test = this.evaluate({ node: node.test, scope, context, forceBindingRootScopeToFunctions });
817835
return test
818-
? this.evaluate({ node: node.consequent, scope, context, allowGlobal, forceBindingRootScopeToFunctions })
819-
: this.evaluate({ node: node.alternate, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
836+
? this.evaluate({ node: node.consequent, scope, context, forceBindingRootScopeToFunctions })
837+
: this.evaluate({ node: node.alternate, scope, context, forceBindingRootScopeToFunctions });
820838

821839
case 'AssignmentExpression':
822-
const value = this.evaluate({ node: node.right, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
840+
const value = this.evaluate({ node: node.right, scope, context, forceBindingRootScopeToFunctions });
823841

824842
if (node.left.type === 'Identifier') {
825843
scope[node.left.name] = value;
826844
return value;
827845
} else if (node.left.type === 'MemberExpression') {
828-
const obj = this.evaluate({ node: node.left.object, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
829-
if (node.left.computed) {
830-
const prop = this.evaluate({ node: node.left.property, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
831-
obj[prop] = value;
832-
} else {
833-
obj[node.left.property.name] = value;
834-
}
835-
return value;
846+
throw new Error('Property assignments are prohibited in the CSP build')
836847
}
837848
throw new Error('Invalid assignment target');
838849

839850
case 'ArrayExpression':
840-
return node.elements.map(el => this.evaluate({ node: el, scope, context, allowGlobal, forceBindingRootScopeToFunctions }));
851+
return node.elements.map(el => this.evaluate({ node: el, scope, context, forceBindingRootScopeToFunctions }));
841852

842853
case 'ObjectExpression':
843854
const result = {};
844855
for (const prop of node.properties) {
845856
const key = prop.computed
846-
? this.evaluate({ node: prop.key, scope, context, allowGlobal, forceBindingRootScopeToFunctions })
857+
? this.evaluate({ node: prop.key, scope, context, forceBindingRootScopeToFunctions })
847858
: prop.key.type === 'Identifier'
848859
? prop.key.name
849-
: this.evaluate({ node: prop.key, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
850-
const value = this.evaluate({ node: prop.value, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
860+
: this.evaluate({ node: prop.key, scope, context, forceBindingRootScopeToFunctions });
861+
const value = this.evaluate({ node: prop.value, scope, context, forceBindingRootScopeToFunctions });
851862
result[key] = value;
852863
}
853864
return result;
@@ -856,6 +867,44 @@ class Evaluator {
856867
throw new Error(`Unknown node type: ${node.type}`);
857868
}
858869
}
870+
871+
checkForDangerousKeywords(keyword) {
872+
let blacklist = [
873+
'constructor', 'prototype', '__proto__',
874+
'__defineGetter__', '__defineSetter__',
875+
'insertAdjacentHTML',
876+
]
877+
878+
if (blacklist.includes(keyword)) {
879+
throw new Error(`Accessing "${keyword}" is prohibited in the CSP build`)
880+
}
881+
}
882+
883+
checkForDangerousValues(prop) {
884+
if (prop === null) {
885+
return
886+
}
887+
888+
if (typeof prop !== 'object' && typeof prop !== 'function') {
889+
return
890+
}
891+
892+
if (safemap.has(prop)) {
893+
return
894+
}
895+
896+
if (prop instanceof HTMLIFrameElement || prop instanceof HTMLScriptElement) {
897+
throw new Error('Accessing iframes and scripts is prohibited in the CSP build')
898+
}
899+
900+
if (globals.has(prop)) {
901+
throw new Error('Accessing global variables is prohibited in the CSP build')
902+
}
903+
904+
safemap.set(prop, true)
905+
906+
return true
907+
}
859908
}
860909

861910
export function generateRuntimeFunction(expression) {
@@ -867,9 +916,9 @@ export function generateRuntimeFunction(expression) {
867916
const evaluator = new Evaluator();
868917

869918
return function(options = {}) {
870-
const { scope = {}, context = null, allowGlobal = false, forceBindingRootScopeToFunctions = false } = options;
919+
const { scope = {}, context = null, forceBindingRootScopeToFunctions = false } = options;
871920
// Use the scope directly - mutations are expected for assignments
872-
return evaluator.evaluate({ node: ast, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
921+
return evaluator.evaluate({ node: ast, scope, context, forceBindingRootScopeToFunctions });
873922
};
874923
} catch (error) {
875924
throw new Error(`CSP Parser Error: ${error.message}`);

packages/docs/src/en/advanced/csp.md

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -106,31 +106,23 @@ The CSP build supports most JavaScript expressions you'd want to use in Alpine:
106106
### Method Calls
107107
```alpine
108108
<!-- ✅ These work -->
109-
<div x-data="{ items: ['a', 'b'], getMessage: () => 'Hello' }">
110-
<span x-text="getMessage()"></span>
109+
<div x-data="{ items: ['a', 'b'] }">
111110
<button x-on:click="items.push('c')">Add Item</button>
112111
</div>
113112
```
114113

115-
### Global Variables and Functions
116-
```alpine
117-
<!-- ✅ These work -->
118-
<div x-data="{ count: 42 }">
119-
<button x-on:click="console.log('Count is:', count)">Log Count</button>
120-
<span x-text="Math.max(count, 100)"></span>
121-
<span x-text="parseInt('123') + count"></span>
122-
<span x-text="JSON.stringify({ value: count })"></span>
123-
</div>
124-
```
125-
126114
<a name="whats-not-supported"></a>
127115
## What's Not Supported
128116

129-
Some advanced JavaScript features aren't supported:
117+
Some advanced and potentially dangerous JavaScript features aren't supported:
130118

119+
### Complex Expressions
131120
```alpine
132121
<!-- ❌ These don't work -->
133-
<div x-data>
122+
<div x-data="{ user: { name: '' } }">
123+
<!-- Property assignments -->
124+
<button x-on:click="user.name = 'John'">Bad</button>
125+
134126
<!-- Arrow functions -->
135127
<button x-on:click="() => console.log('hi')">Bad</button>
136128
@@ -145,6 +137,28 @@ Some advanced JavaScript features aren't supported:
145137
</div>
146138
```
147139

140+
### Global Variables and Functions
141+
```alpine
142+
<!-- ❌ These don't work -->
143+
<div x-data>
144+
<button x-on:click="console.log('hi')"></button>
145+
<span x-text="document.title"></span>
146+
<span x-text="window.innerWidth"></span>
147+
<span x-text="Math.max(count, 100)"></span>
148+
<span x-text="parseInt('123') + count"></span>
149+
<span x-text="JSON.stringify({ value: count })"></span>
150+
</div>
151+
```
152+
153+
### HTML Injection
154+
```alpine
155+
<!-- ❌ These don't work -->
156+
<div x-data="{ message: 'Hello <span>World</span>' }">
157+
<span x-html="message"></span>
158+
<span x-init="$el.insertAdjacentHTML('beforeend', message)"></span>
159+
</div>
160+
```
161+
148162
<a name="when-to-extract-logic"></a>
149163
## When to Extract Logic
150164

0 commit comments

Comments
 (0)