Skip to content

Don't assign the normalEvaluator per default#4548

Open
JeroenBoersma wants to merge 4 commits intoalpinejs:mainfrom
JeroenBoersma:main
Open

Don't assign the normalEvaluator per default#4548
JeroenBoersma wants to merge 4 commits intoalpinejs:mainfrom
JeroenBoersma:main

Conversation

@JeroenBoersma
Copy link

Assigning the normal evaluator to theEvaluatorFunction means that it always needs to be exported.

CSP doesn't use the normalEvaluator and should not include it in any build. alpinsjs/src/index.js always assigns the normalEvaluator manually and works as expected, and it isn't included anymore in the csp builds.

  • normalEvaluator also needed dependencies for unsafe functions which we don't want to include in the csp build

Assigning the normal evaluator to theEvaluatorFunction means that it
always needs to be exported.

CSP doesn't use the normalEvaluator and should not include it in any
build. alpinsjs/src/index.js always assigns the normalEvaluator manually
and works as expected, and it isn't included anymore in the csp builds.

* normalEvaluator also needed dependencies for unsafe functions which we
  don't want to include in the csp build
Copy link
Contributor

@ekwoka ekwoka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it!

JeroenBoersma added a commit to JeroenBoersma/livewire that referenced this pull request Feb 24, 2025
* I forgot to redirect some alpinejs to @/alpine
* tested build on my alpinejs merge request alpinejs/alpine#4548
JeroenBoersma and others added 3 commits April 9, 2025 17:46
+ it would really helpfull for developers to have sourcemap files alongside of the js and minified js files
The sourcemap change is unrelated to the CSP evaluator fix and
should be a separate PR if desired.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@calebporzio
Copy link
Collaborator

PR Review: #4548 — Don't assign the normalEvaluator per default

Type: Bug fix
Verdict: Merge

What's happening (plain English)

  1. packages/alpinejs/src/evaluator.js exports both normalEvaluator (which uses new Function/new AsyncFunction — i.e. eval-like constructs) and setEvaluator().
  2. On main, line 31 reads: let theEvaluatorFunction = normalEvaluator — this assigns the unsafe evaluator as the module-level default.
  3. The CSP build (packages/csp/src/evaluator.js) imports generateEvaluatorFromFunction from this same file. Because theEvaluatorFunction references normalEvaluator at the module scope, the bundler (esbuild) cannot tree-shake normalEvaluator or its dependency chain out of the CSP bundle.
  4. Result: the CSP build ships new Function() and new AsyncFunction() — the exact things it exists to avoid.
  5. The fix: change the default to () => {}. Both the normal build and CSP build already call setEvaluator() explicitly in their respective index.js entry points (lines 28 and 22), so the default value is never actually used at runtime.

I verified this empirically:

  • Without fix: CSP bundle contains 4 occurrences of AsyncFunction/new Function, bundle size 17.8 KB
  • With fix: CSP bundle contains 0 occurrences, bundle size 17.5 KB

Other approaches considered

  1. Mark normalEvaluator with /*#__PURE__*/ annotations — wouldn't help because the issue is the module-level assignment, not function call tree-shaking.
  2. Make normalEvaluator lazy (getter) — more complex than needed. The entry points already call setEvaluator() explicitly, so a no-op default is the simplest correct solution.
  3. Move normalEvaluator to a separate file — unnecessary refactor. The one-line change achieves the same result.

The PR's approach is the laziest correct solution. One line change, zero risk.

Changes Made

  • Removed unrelated sourcemap: true addition from scripts/build.js — The PR included a second commit adding sourcemaps to CDN builds. This is unrelated to the CSP evaluator fix and was removed to keep the PR focused. If sourcemaps are desired, that should be a separate PR.
  • Whitespace-only changes in evaluator.js (trailing whitespace removal) are harmless and left as-is.

Test Results

  • tests/vitest/evaluator.spec.js: 25 passed, 1 skipped ✅
  • tests/vitest/csp-evaluator.spec.js: 11 passed ✅
  • tests/cypress/integration/plugins/csp-compatibility.spec.js: 17 passed ✅
  • CI: build passing ✅

No new tests needed — this is a build/bundling optimization. The existing CSP tests verify the CSP evaluator works correctly, and the evaluator tests verify the normal evaluator works. The fix was verified empirically by inspecting bundle output.

Code Review

The core change (packages/alpinejs/src/evaluator.js:31) is a one-line fix that's correct and safe:

// Before (main):
let theEvaluatorFunction = normalEvaluator

// After (this PR):
let theEvaluatorFunction = () => {}

Both packages/alpinejs/src/index.js:28 and packages/csp/src/index.js:22 call setEvaluator() before Alpine starts, so the no-op default is never invoked at runtime.

Security

This PR improves security. The CSP build was inadvertently shipping new Function() and new AsyncFunction() — exactly the unsafe-eval constructs that CSP policies are designed to block. While these functions weren't being called in the CSP build (since setEvaluator overrides them), their mere presence in the bundle meant stricter CSP policies that block eval-like code from even being parsed could reject the bundle. This fix eliminates that entirely.

Verdict

Merge. This is a clean, one-line fix for a real problem. The CSP build was shipping unsafe-eval code it shouldn't have. The fix is the simplest possible approach, verified empirically, all tests pass, and it has community support (1 approval from @ekwoka, 3 thumbs-up reactions, 1 rocket). I removed the unrelated sourcemap commit to keep the PR focused.


Reviewed by Claude

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants