Skip to content

Add JSON option to @routes for improved CSP compatibility #831

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: 2.x
Choose a base branch
from

Conversation

flexponsive
Copy link

This pull request introduces a new asJson boolean option to the @routes Blade directive, improving compatibility with strict Content Security Policies (CSP).

When asJson is set to true, the directive outputs the Ziggy configuration as a JSON object inside a <script> tag with type="application/json". This type is treated by browsers as inert data (not executable code), which allows the directive to work even under strict script-src policies—without requiring a nonce.

The Ziggy JavaScript (specifically useRoute() and route()) has been updated to detect when the global Ziggy object is missing and, in that case, fallback to reading the configuration from a script tag with the ID Ziggy_routes.

This approach simplifies CSP configuration for Ziggy users, since it removes the need to configure or pass a nonce when CSP is enforced.

Usage

@routes(asJson: true)

Testing

Manual testing:
Tested on macOS in Chrome, Safari, and Firefox with CSP enabled.

Automated tests:

  • Added backend tests to verify Blade output.
  • Added frontend tests to confirm useRoute() works with JSON-based configuration.

@bakerkretzmar bakerkretzmar self-assigned this Apr 16, 2025
@bakerkretzmar
Copy link
Collaborator

Is there functionally any difference between this and using a nonce? Like are there any CSP directives that would disallow a nonce but allow this, and if so is there any reason to prefer that approach? I'm definitely open to this if it provides concrete advantages over Ziggy's current functionality.

@flexponsive
Copy link
Author

Thanks for the quick response to the PR!

Here's the CSP header I use across several projects:

default-src 'self'; script-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; frame-ancestors 'self'; object-src 'none'; base-uri 'self'

In this case, there's no nonce in use—but the JSON-based approach still works seamlessly. Avoiding the nonce has a few practical advantages:

  • Security: CSP headers can be set at the HTTP server level rather than within the application, reducing the risk that they’re disabled by accident. It also avoids a class of vulnerabilities tied to weak or reused nonces (which can happen, for example, with full-page caching or misconfigured middlewares).

  • Simplicity: The current @routes directive converts JSON into executable JavaScript, which then has to be explicitly allowed via a nonce. With the JSON approach, the data is inert and doesn’t require any CSP exceptions—so it feels like a more natural fit.

This also ties into a broader benefit: improving CSP compatibility of Laravel out of the box. For example, if you enable a strict CSP like the one above on a fresh Laravel Starter Kit install, the only two things that break are the progress bar and Ziggy. With this PR, it would probably be a one-line change in the starter kit to get the Ziggy part compliant with strict CSP.

@flexponsive
Copy link
Author

I’ve moved the config loading logic into Router.js and tested the updated implementation with the current Vue Starter Kit.

I can confirm that Ziggy now works with a strict CSP (script-src 'self') simply by setting @routes(asJson: true) in your Blade template. This is significantly simpler than creating a custom middleware and injecting the Vite nonce or configuring a third-party CSP package to support nonces.

Thanks for considering this PR!

@flexponsive flexponsive marked this pull request as ready for review April 16, 2025 21:56
@flexponsive
Copy link
Author

I’ve made another small update: the ZiggyVue plugin now creates the global window.route function during installation if it doesn't already exist.

This addresses a subtle issue in Vue 3 when using <script setup>. In that context, accessing route() directly doesn't use app.config.globalProperties.route, but instead falls back to window.route. This leads to unexpected behavior if window.route is undefined (when using the new asJson mode)

Example:

<script setup>
const globalRoute = route('login'); // uses window.route
</script>

<template>
  <p>{{ route('login') }}</p> <!-- uses app.config.globalProperties.route -->
</template>

By ensuring window.route is defined during plugin installation (if not already set), both usage patterns now work consistently out of the box. I also added a test case.

@bakerkretzmar let me know what you think!

@bakerkretzmar
Copy link
Collaborator

@flexponsive I think this is looking good. I made a few tweaks:

  • moved the JSON output into its own Output class to keep it separate from everything else and not need to pass the option around as much
  • assigned the parsed JSON to globalThis.Ziggy after it's parsed so that we don't have to find the DOM element and parse the JSON on every route() call
  • removed assigning the route function to window.route in the Vue plugin - this is more consistent with how Ziggy generally works in other contexts where you don't just use plain @routes somewhere in your Blade template, e.g. with SPAs. if you choose to not make the route() function available globally, to use it in <script setup> you need to const route = inject('route'); when necessary (or make it available globally yourself)

@bakerkretzmar bakerkretzmar changed the title Add asJson option to @routes for improved CSP compatibility Add JSON option to @routes for improved CSP compatibility May 17, 2025
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.

2 participants