Skip to content
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

[9.x] Support CSP nonce, SRI, and arbitrary attributes with Vite #43442

Merged
merged 2 commits into from
Jul 29, 2022

Conversation

timacdonald
Copy link
Member

@timacdonald timacdonald commented Jul 27, 2022

This PR introduces support for three distinct, but related, Vite features. Content security policy nonce (security), sub-resource integrity (security), and arbitrary tag attributes (security / app specific features).

They are combined in a single PR to create a holistic solution, as they all share the same problem space with regards to implementation, which is putting attributes on a HTML tag.

Content Security Policy (CSP) Nonce

You can read about what a CSP nonce is over on the MDN documentation, but summary it is a security feature where all script and style tags must contain the same nonce specified by the Content-Security-Policy header or else the browser will not execute them.

Our Vite implementation now has first party support for creating a nonce that will be used on all generated script and style tags.

Warning: You still need to return the content security policy header manually.

Here is an example of its usage:

use Illuminate\Support\Facades\Vite;

class ContentSecurityPolicy
{
    public function handle($request, $next)
    {
        // You need to call this function **before** resolving the response
        // so that 3rd parties can use the same nonce throughout the request
        // lifecycle e.g. Ziggy

        Vite::useCspNonce();

        return $next($response)->withHeaders([
            'Content-Security-Policy' => "script-src 'nonce-".Vite::cspNonce()."'",
        ]);
    }
}

All script and style tags generated with Vite will now have the nonce="{cryptographically_secure_nonce}" attribute.

<link
    rel="stylesheet"
    href="https://laravel.com/build/assets/app.j3j28sk1l.css"
    nonce="{cryptographically_secure_nonce}"
/>

You can additionally retrieve the nonce throughout your application, for example in you blade views:

{{-- Use Ziggy with the same nonce --}}
{{-- See: https://github.com/tighten/ziggy#using-routes-with-a-content-security-policy --}}

@routes(nonce: Vite::cspNonce())

If you do not want to use Laravel's default nonce generation, and would like to use your own, you may pass a nonce to the Vite::useCspNonce() function. When generation tags, Vite will then use the given value in the nonce attribute.

use Illuminate\Support\Facades\Vite;

class CspMiddleware
{
    public function handle($request, $next)
    {
        // You need to specify this **before** resolving the response...

        Vite::useCspNonce(generateMyOwnNonce());

        return $next($response)->withHeaders([
            'Content-Security-Policy' => "script-src 'nonce-".Vite::cspNonce()."'",
        ]);
    }
}

When specifying your own nonce to use, it is still possible to retrieve the value specified via the Vite class as seen above:

@routes(nonce: Vite::cspNonce())

Subresource integrity (SRI)

You can read about what a SRI nonce is over on the MDN documentation, but summary it is a security feature where the hash of the resource contents must match the hash in the integrity attribute of the tag loading the resource, or else the browser will not execute them.

By convention, Laravel's Vite integration works on the convention of having an "integrity" property specified in the manifest for the given chunk. Here is an example manifest:

{
  "resources/js/app.js": {
    "file": "assets/app.88ad0043.js",
    "src": "resources/js/app.js",
    "isEntry": true,
    "integrity": "sha384-NLhywiqRzeFT85dJzotJfHlfYU3tGCDj5BshD4to8kXVQZwrhTrluhYFbweDmEyL"
  },
  "resources/css/app.css": {
    "file": "assets/app.3f8f484f.css",
    "src": "app.css",
    "isEntry": true,
    "integrity": "sha384-e3Eztracy2WEqhl/vLFMx+hjnDmvYQnO7pLNvrDeUfMU9T7hq+M/Iv9ace+GCbBV"
  }
}

When chunks within the Vite manifest has the given shape, generated tags will contain have the "integrity" attribute added.

<link
    rel="stylesheet"
    href="https://laravel.com/build/assets/app.versioned.css"
    integrity="sha384-e3Eztracy2WEqhl/vLFMx+hjnDmvYQnO7pLNvrDeUfMU9T7hq+M/Iv9ace+GCbBV"
/>
<script
    type="module"
    src="https://laravel.com/build/assets/app.versioned.js"
    integrity="sha384-NLhywiqRzeFT85dJzotJfHlfYU3tGCDj5BshD4to8kXVQZwrhTrluhYFbweDmEyL"
></script>

But generating these integrity tags manually in each application is a PITA. I recommend instead of recreating the wheel, to lean on the vite-plugin-manifest-sri. When using this plugin, everything will "Just Work™️" without any additional work by the developer.

So to setup SRI in your application, you can install the plugin:

npm i -D vite-plugin-manifest-sri

Add it to your vite.config.js

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
+ import manifestSRI from 'vite-plugin-manifest-sri'

export default defineConfig({
    plugins: [
        laravel({
            // ...
        }),
+       manifestSRI(),
    ],
});

...and Bob's your uncle.

If you use a different plugin that does not use the "integrity" key in the manifest, you may configure the key used by Vite to find the integrity hash. If the plugin creates the following manifest structure...

{
  "resources/js/app.js": {
    "file": "assets/app.88ad0043.js",
    "src": "resources/js/app.js",
    "isEntry": true,
-   "integrity": "sha384-NLhywiqRzeFT85dJzotJfHlfYU3tGCDj5BshD4to8kXVQZwrhTrluhYFbweDmEyL"
+   "SRI": "sha384-NLhywiqRzeFT85dJzotJfHlfYU3tGCDj5BshD4to8kXVQZwrhTrluhYFbweDmEyL"
  },
  "resources/css/app.css": {
    "file": "assets/app.3f8f484f.css",
    "src": "app.css",
    "isEntry": true,
-   "integrity": "sha384-e3Eztracy2WEqhl/vLFMx+hjnDmvYQnO7pLNvrDeUfMU9T7hq+M/Iv9ace+GCbBV"
+   "SRI": "sha384-e3Eztracy2WEqhl/vLFMx+hjnDmvYQnO7pLNvrDeUfMU9T7hq+M/Iv9ace+GCbBV"
  }
}

You can tell Vite to check the "SRI" key in the manifest instead:

Illuminate\Support\Facades\Vite::useIntegrityKey('SRI');

If you want to disable this auto-detection completely, you can opt out of it:

Illuminate\Support\Facades\Vite::useIntegrityKey(false);

Arbitrary attributes

The above features have been about providing first party support for some key security features with loaded script / css resources, however it is also useful for developers to be able to add arbitrary attributes to the generated tags without creating first party solutions for everything under the sun.

With the following API, developers may specify attributes to add to their script and stylesheet tags.

use Illuminate\Support\Facades\Vite;

Vite::useAttributesForScriptTag([
    'data-turbolinks-track' => 'reload',
]);

Vite::useAttributesForStyleTag([
    'data-turbolinks-track' => 'reload',
]);

If you need to conditionally add attributes to tags based on the chunk, you may use a callback that receives the src, url, chunk, and the entire manifest:

use Illuminate\Support\Facades\Vite;

Vite::useAttributesForScriptTag(fn (string $src, string $url, array|null $chunk, array|null $manifest) => [
    //
]);

Vite::useAttributesForStyleTag(fn (string $src, string $url, array|null $chunk, array|null $manifest) => [
    //
]);

Note: The $chunk and $manifest arguments will be null in HMR mode.

The attributes specified have some logic behind them to be aware of...

Vite::useAttributesForStyleTag([
    'attribute-only',
    'attribute-with' => 'value',
    'attribute-with-zero' => 0,
    'attribute-with-true-value' => true,
    'attribute-with-false-value' => false,
    'attribute-with-empty-string' => '',
    'attribute-with-null' => null,
]);

The notable things here are that attributes with values === false will filtered out, while true values will return only the attribute, which is the HTML attribute conventions.

<script
    type="module"
    src="..."
    attribute-only
    attribute-with="value"
    attribute-with-zero="0"
    attribute-with-true-value
    attribute-with-empty-string=""
></script>

Note that:

  • attribute-with-false-value is not present in the output HTML. We filter them out as false attributes are simply missing attributes.
  • attribute-with-empty-string is present and maintains it's empty string value. This is considered a true attribute in HTML land.
  • attribute-with-null is not present in the output HTML. I wasn't 100% sure about this, but I feel that in HTML an "empty" value is a missing attribute, so this feels like the right move to me.

You can read more about how boolean attributes work in the HTML specification.

Finally, any attributes you return from the closure will be merged with the default attributes - but your returned attributes will have precedence - so you have the ability to override anything and everything if you have some wild use-case where you need to do that.

Introducing these in a non-breaking way

Some of the changes I've made feel a little bit funky in one spot, as I've needed to keep the current functionality in place to not break current functionality for those who have extended the Vite class, while introducing new functionality that requires different method signatures.

As such, I've left the old makeTag function in place and that continues to be called unless any of these new features is utilised. I've deprecated 3 methods and we can clean them up in 10.x.

API

I've built this using a static API. I would have preferred to go down the path of an instance API, but I felt for this it did make sense to match the more static style API even thought it is not a Facade. If we want to pursue an instance API we would need to bind the Vite class as a singleton and then have the API used as follows:

Documentation

I'll work on a follow up PR for the docs.

@timacdonald
Copy link
Member Author

FYI: Support for nonce / arbitrary attributes in HMR mode has not yet been pushed, but will be supported before the PR is merged.

Just need to discuss it a little more with the team on how we wanna handle a few things.

@driesvints
Copy link
Member

Using Singleton & a Facade 👍

@tonysm
Copy link
Contributor

tonysm commented Jul 28, 2022

Shouldn't the CSP stuff be independent of Vite? I know that's the main use case, but other setups could still benefit from Laravel having a default CSP implementation without using Vite and all. Either way, nice addition!

@valorin
Copy link
Contributor

valorin commented Jul 29, 2022

I haven't dived into Vite yet, but this is awesome and I'm definitely going to upgrade to Vite now. 😁
Having it automatically generate tags, add SRI and the nonce is great, and adding nonces on inline scripts sounds easy too (<script nonce="{{ Vite::cspNonce() }}">...</script>). 👍

@tonysm it needs to be tied into the Vite implementation as that automatically generates the script and style tags on the page. So getting a nonce and SRI onto those tags needs to be supported by Vite anyway, this just makes it easier.

Implementing a CSP nonce without this is still fairly trivial with View::share(): https://gist.github.com/valorin/d4cb9daa190fdee90603efaa8cbc5886

But I do agree that having CSP in Laravel's core would be a really nice addition. All the developer would need to do* is tweak a config file to define the rules, toggle report-only, set a reporting endpoint, etc. (I'd PR it myself if I actually had some free time...)

* Famous last words...

@tonysm
Copy link
Contributor

tonysm commented Jul 29, 2022

@valorin the Vite implementation could rely on the core CSP stuff under the hood (the same way other setups would have to)

@valorin
Copy link
Contributor

valorin commented Jul 29, 2022

@tonysm Oh I completely agree, but until that becomes a reality, having it in Vite like this is better than nothing.
Correct me if I'm wrong, but the only bit that isn't Vite specific here is the actual nonce generation?
That could be extracted into a standalone Nonce or CSP helper that devs can use directly. 🤔

@timacdonald
Copy link
Member Author

timacdonald commented Jul 29, 2022

Really appreciate the feedback here folks ❤️

I've tried build this with future enhancements / integrations in mind.

Taking Spatie's CSP package as an example, you could do either of the following...

  1. Create a generator that just uses Laravel Vite's one under the hood.

See the default generator that ships with the package.

<?php

namespace Spatie\Csp\Nonce;

use Illuminate\Support\Str;
use Illuminate\Support\Facades\Vite;

class LaravelVite implements NonceGenerator
{
    public function generate(): string
    {
        return Vite::useNonce();
    }
}

or coming at it the other way, you could still generate your own nonce, and just tell Vite about the nonce to use...

<?php

namespace Spatie\Csp\Nonce;

use Illuminate\Support\Str;
use Illuminate\Support\Facades\Vite;

class RandomString implements NonceGenerator
{
    public function generate(): string
    {
        return Vite::useNonce(
            Str::random(32)
        );
    }
}

If there is a usecase I have not considered, please let me know. I would love to dive into it.

But keep in mind this PR isn't about making CSP headers a first party solution, but making it so that you can use your own CSP header, something from a 3rd party, or potentially something first party further down the line.

@tonysm
Copy link
Contributor

tonysm commented Jul 29, 2022

@valorin Yep, only that part. Maybe making some helper functions and stuff, like csp_nonce and stuff, but yeah, just that part, since this PR is not about making a full CSP implementation (with the headers, configs, etc.)

@timacdonald Got it. 👍🏼

@valorin
Copy link
Contributor

valorin commented Jul 29, 2022

@timacdonald Yep, makes sense. As you said, it's trivial to pass in an externally generated nonce, or pass your nonce into anything that needs it, so I'm happy.

Is CSP support something that would be considered at some point, or out of scope?
It's not a set-and-forget type security feature like the others in Laravel, so I'm guessing it's not a simple feature to add?

@timacdonald timacdonald force-pushed the vite-nonce branch 2 times, most recently from 9cc5108 to 27fdf70 Compare July 29, 2022 06:15
@timacdonald timacdonald marked this pull request as ready for review July 29, 2022 06:28
@timacdonald timacdonald mentioned this pull request Jul 29, 2022
@timacdonald
Copy link
Member Author

@valorin all I can really say is that it isn't something we are actively working on. Doesn't mean it is out of scope or not something we will consider in the future however.

@taylorotwell taylorotwell merged commit 9a6bfc4 into laravel:9.x Jul 29, 2022
@timacdonald timacdonald deleted the vite-nonce branch August 7, 2022 23:58
Ken-vdE pushed a commit to Ken-vdE/framework that referenced this pull request Aug 9, 2022
…avel#43442)

* Support CSP nonce, SRI, and arbitrary attributes with Vite

* formatting

Co-authored-by: Taylor Otwell <taylor@laravel.com>
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.

5 participants