Alpine.js CSP Build: Strict Content Security Policy Without eval()
AI generated
x-data
Alpine
Alpine.js · CSP · Web Security · Nonce
Alpine.js CSP Build
strict Content Security Policy without eval()

The default Alpine.js build evaluates expressions in x-data and x-on through new Function(), which gets blocked immediately under a strict Content Security Policy without unsafe-eval. The dedicated Alpine.js CSP build replaces that evaluation with a safe expression parser, making reactive components possible even in environments with the strictest security policy.

16 min read CSP build · Alpine.data() · nonce · migration Alpine.js 3.x · @alpinejs/csp

1. Why the default Alpine.js build needs unsafe-eval

Alpine.js evaluates expressions like x-data="{ open: false }" or x-on:click="open = !open" at runtime by passing the string as a function body to new Function(). That is exactly why developers can write arbitrary JavaScript expressions directly inside an HTML attribute without compiling anything beforehand. But this very ability to turn strings into code falls under unsafe-eval in a Content Security Policy, because the same technique can be abused for cross-site scripting.

In environments with a strict security policy, such as banks, government sites, browser extensions or Electron apps, unsafe-eval is usually forbidden entirely because it would undermine one of the most important protections of the CSP. This is exactly the case the dedicated Alpine.js CSP build was built for, offering the same feature set without new Function() and without eval().

The rest of this article shows how the Alpine.js CSP build works internally, which syntax restrictions it brings, how Alpine.data() becomes the central solution, and how migrating an existing project to a strict Content Security Policy plays out in practice.

2. Content Security Policy: script-src and the role of unsafe-eval

A Content Security Policy is set through the Content-Security-Policy HTTP header or a meta tag and restricts which script sources a browser is allowed to execute at all. The directive script-src 'self' only allows scripts from the same domain, but by default it also blocks eval(), new Function() and similar dynamic code evaluation mechanisms, unless 'unsafe-eval' is explicitly added.

unsafe-eval carries the word unsafe for good reason: if an attacker finds a way to inject controllable string input into an eval-like function, for instance through an improperly escaped templating variable, the result can be arbitrary code execution in the context of the page. That is why many security audits and compliance requirements, for instance in the PCI-DSS space, forbid unsafe-eval outright. The Alpine.js CSP build is exactly the answer to that requirement, without having to give up Alpine as a framework.

3. Including the Alpine.js CSP build

Instead of the default alpinejs package, strict environments include the @alpinejs/csp package. It exports the same global Alpine API, but internally uses a hand-written expression parser instead of new Function(). That means most existing templates keep working unchanged, only certain advanced JavaScript constructs in inline expressions are no longer allowed.


// package.json — swap the default build for the CSP-compatible one
{
  "dependencies": {
    "@alpinejs/csp": "^3.14.0"
  }
}

// resources/js/app.js
import Alpine from '@alpinejs/csp'

window.Alpine = Alpine
Alpine.start()

// CSP header that would block the default Alpine build entirely
// Content-Security-Policy: script-src 'self'; object-src 'none';
// No 'unsafe-eval' needed with @alpinejs/csp

It matters that the Alpine.js CSP build requires no compile step or build tooling. It is a drop-in replacement that runs at runtime in the browser, but internally relies on a tokenizer and parser for the limited Alpine expression grammar instead of the generic JavaScript evaluation of new Function(). That explains why some expressions that work in the default build produce a parser error in the CSP build.

4. Alpine.data(): moving expressions from HTML into JavaScript

The central strategy in the Alpine.js CSP build is to stop writing complex logic as an inline string in an HTML attribute and instead register it as a named component through Alpine.data(), which lives in a regular JavaScript file that is not affected by the CSP. The HTML then only references the component by its name, for instance x-data="dropdown", without complex code inside the attribute itself.


// resources/js/components/dropdown.js
// Registered once, outside the CSP-restricted inline attribute
document.addEventListener('alpine:init', () => {
    Alpine.data('dropdown', () => ({
        open: false,
        items: [],
        toggle() {
            this.open = !this.open
        },
        select(item) {
            this.selected = item
            this.open = false
        },
    }))
})

<!-- resources/views/dropdown.blade.php -->
<!-- Only a simple identifier reference, no inline JS expression -->
<div x-data="dropdown">
    <button x-on:click="toggle">Open menu</button>
    <ul x-show="open">
        <template x-for="item in items">
            <li x-on:click="select(item)" x-text="item.label"></li>
        </template>
    </ul>
</div>

This relocation has an important side effect: methods like toggle or select are now real JavaScript functions in a regular file, covered by linting, type checking and tests. The Alpine.js CSP build thereby indirectly enforces a cleaner separation between markup and logic, which is a quality improvement even without a strict CSP requirement.

5. What no longer works in the CSP build

The expression parser of the Alpine.js CSP build supports a restricted subset of JavaScript directly inside HTML attributes. Forbidden are arbitrary function definitions as an inline expression, such as x-on:click="() => { doSomething() }", as well as multi-line statement blocks separated by semicolons in more complex forms. Simple method calls, property access, ternary expressions and comparison operators remain allowed.

Complex inline code in x-init should also be avoided in the Alpine.js CSP build and instead implemented as an init() method inside the Alpine.data() definition. The parser is deliberately conservative, because every additional language feature it supports potentially opens a new way to inject code, even if that path is technically no longer eval().

6. Combining a nonce strategy for inline scripts

The Alpine.js CSP build only solves the problem of dynamic expression evaluation, not the general problem of inline <script> tags. Anyone who still needs an inline script for initialization code, for instance to register Alpine.data() components directly in the template, must attach a server-generated nonce attribute to that script, matching the script-src 'nonce-...' value in the CSP header.


// Server-side: generate a fresh nonce per request and reuse it in both
// the CSP header and every inline script tag that must run.
// Content-Security-Policy: script-src 'self' 'nonce-r4nd0mVal123';

<script nonce="r4nd0mVal123">
    document.addEventListener('alpine:init', () => {
        Alpine.data('counter', () => ({ count: 0 }))
    })
</script>

It matters that the nonce value is freshly generated on every request and never hardcoded in the template, otherwise it loses its security value entirely. In most frameworks, for instance Laravel or Symfony, there are middleware packages that generate the nonce automatically and insert it into both the response header and a template variable. The Alpine.js CSP build and the nonce strategy complement each other, because both independently close different attack surfaces.

7. Migrating existing components step by step

Migrating a grown project from the default build to the Alpine.js CSP build works best step by step rather than in one large rewrite. First, the CSP build is tested in parallel in the development environment with an enabled but not yet enforced CSP, for instance through the Content-Security-Policy-Report-Only header, which only logs violations without blocking them.

Every component that triggers a parser error is identified and switched to Alpine.data(). In practice this usually affects a small share of complex components with nested callbacks, while simple x-show and x-model bindings keep working unchanged. Only once report-only mode reports no more violations is the CSP actually enforced in the next step.

8. Common mistakes when switching to the CSP build

The most common mistake is continuing to use complex inline arrow functions in x-on attributes and then wondering why the console reports a parser error that never occurred in the default build. The second common mistake is importing the CSP build but keeping unsafe-eval in the CSP header out of habit, which erases the actual security gain because other scripts on the page are still allowed to use eval.


// WRONG: complex inline arrow function — fails in the CSP build's parser
<button x-on:click="() => { fetch('/api/like').then(r => r.json()) }">
    Like
</button>

// RIGHT: named method on an Alpine.data() component
document.addEventListener('alpine:init', () => {
    Alpine.data('likeButton', () => ({
        async like() {
            const response = await fetch('/api/like')
            this.liked = await response.json()
        },
    }))
})

<button x-data="likeButton" x-on:click="like">Like</button>

// WRONG: importing @alpinejs/csp but keeping unsafe-eval in the CSP header
// Content-Security-Policy: script-src 'self' 'unsafe-eval';  ← defeats the purpose

// RIGHT: drop unsafe-eval entirely once migration is complete
// Content-Security-Policy: script-src 'self' 'nonce-r4nd0mVal123';

9. Default build vs. CSP build compared

The decision between the default build and the Alpine.js CSP build depends on the project's security requirements. The following table compares both variants.

Aspect Default build Alpine.js CSP build Consequence
CSP compatibility Needs unsafe-eval Runs without unsafe-eval CSP build mandatory for strict environments
Inline expressions Arbitrary JavaScript Restricted grammar Move complex logic into Alpine.data()
File size Marginally smaller Marginally larger, own parser Practically negligible
Code organization Logic often scattered in HTML Logic centralized in JS files Better testability as a side effect
Migration effort None, it is the default Depends on number of complex inline expressions Start step by step with report-only

10. Summary

The Alpine.js CSP build makes reactive Alpine components possible even under a strict Content Security Policy without unsafe-eval, by replacing new Function() with its own expression parser. Complex inline logic must be moved into Alpine.data() components for this, which simultaneously improves code organization. Nonce-based inline scripts complement the CSP build for the remaining necessary script tags.

For projects without strict compliance requirements, the default build remains the more convenient choice. But as soon as a security audit, an authority or an industry requirement excludes unsafe-eval, the Alpine.js CSP build is the direct path to keep using Alpine without switching frameworks. Step by step migration with a report-only header minimizes the risk of unexpected breakages in production.

Alpine.js CSP build: the essentials at a glance

Reason

Default Alpine needs new Function() and therefore unsafe-eval. The CSP build replaces that with a safe parser.

Core strategy

Move complex logic out of inline attributes into Alpine.data() components, HTML only as a reference.

Restriction

No inline arrow functions or multi-line statement blocks allowed directly in the attribute.

Combination

Nonce strategy for remaining inline scripts, migration step by step through a report-only header.

11. FAQ: Alpine.js CSP Build

1Why does Alpine.js need unsafe-eval?
Because expressions are evaluated through new Function(), which falls under the unsafe-eval directive of the CSP.
2How do I install the CSP build?
Install package @alpinejs/csp instead of alpinejs. Drop-in replacement with identical API.
3Do all templates work unchanged?
Simple bindings yes, complex inline arrow functions must be moved into Alpine.data().
4What is Alpine.data() here?
Registering component logic in regular JavaScript, HTML only references the name.
5Does the CSP build also fix inline scripts?
No, a nonce strategy is additionally needed for the remaining script tags.
6Test migration safely?
With Content-Security-Policy-Report-Only, which logs violations without blocking them.
7Is the CSP build slower?
The difference is negligible in practice, the own parser is only marginally larger.
8Can I still use x-init?
Simple expressions yes, complex code belongs in an init() method inside Alpine.data().
9Remove unsafe-eval immediately?
Yes, otherwise the security gain is lost. Remove only after a clean report-only test and then enforce the policy.
10Which projects benefit most?
Projects with strict compliance requirements, for instance finance, government or browser extension environments.