Oh Dear is the all-in-one monitoring tool for your entire website. We monitor uptime, SSL certificates, broken links, scheduled tasks and more. You'll get a notifications for us when something's wrong. All that paired with a developer friendly API and kick-ass documentation. O, and you'll also be able to create a public status page under a minute. Start monitoring using our free trial now.

Using Content Security Policy headers in a Laravel app

Original – by Freek Van der Herten – 5 minute read

By default all scripts on a webpage are allowed to send and fetch data from and to any site they want. If you think about it, that's kinda scary. Imagine that one of your JavaScript dependencies would send all keystrokes, including passwords, to a third party website. That would be pretty bad.

In this blogpost I'd like to give a little bit more background on the problem and present a solution in the form of our newly released laravel-csp package.

Introducing CSP

You might think that yeah, I'm not going to run rogue JavaScript. And while you might use well known JS packages, are you really sure their dependencies (and their dependencies, etc...) don't contain unwanted code? It's actually very easy for someone to hide malicious behaviour. To really feel the problem you should read this excellent blog post by David Gilbertson. Spoiler: it's nearly impossible for you to detect rogue JavaScript (unless you manually read all the JavaScript code on your site).

Luckily there's a good solution to this problem. Every browser has support for secure content policies. In short it works as follows: you set an http header named Content-Security-Policy. Its value contains a description of all sources where content may load from. So it's not constricted to JavaScript, you can also determine of which sources images, styles, etc... can be loaded from. You can even specify to which hosts the forms on your site are allowed to post to. For more info on CSP itself and which directives you can use in it, head over to Mozilla's excellent documentation on CSP.

Implementing CSP in a Laravel app

To easily add a Content Security Policy to a Laravel app, our team at Spatie has created a new package called laravel-csp. Once installed it allows you to create a policy class. It can look like this:

namespace App\Services\Csp\Policies;

use Spatie\Csp\Directive;
use Spatie\Csp\Policies\Policy;

class MyCustomPolicy extends Policy
{
    public function configure()
    {
        parent::configure();
        
        $this->addDirective(Directive::SCRIPT, 'www.google.com');
    }
}

This policy will allow you to run everything from your own site (that's being handled in the base policy Spatie\Csp\Profiles\Policy) and it allows script to load from the www.google.com domain.

On real-life sites policies will probably be a bit bigger, because on most sites a lot of 3th party services are used. Here is the policy used on this very page you're reading:

namespace App\Services\Csp;

use Spatie\Csp\Directive;
use Spatie\Csp\Policies\Policy as BasePolicy;

class Policy extends BasePolicy
{
    public function configure()
    {
        $this
            ->addGeneralDirectives()
            ->addDirectivesForBootstrap()
            ->addDirectivesForCarbon()
            ->addDirectivesForGoogleFonts()
            ->addDirectivesForGoogleAnalytics()
            ->addDirectivesForGoogleTagManager()
            ->addDirectivesForTwitter()
            ->addDirectivesForYouTube();
    }

    protected function addGeneralDirectives(): self
    {
        return $this
            ->addDirective(Directive::BASE, 'self')
            ->addNonceForDirective(Directive::SCRIPT)
            ->addDirective(Directive::SCRIPT, [
                'murze.be',
                'murze.be.test',
            ])
            ->addDirective(Directive::STYLE, [
                'murze.be',
                'murze.be.test',
                'unsafe-inline',
            ])
            ->addDirective(Directive::FORM_ACTION, [
                'murze.be',
                'murze.be.test',
                'sendy.murze.be',
            ])
            ->addDirective(Directive::IMG, [
                '*',
                'unsafe-inline',
                'data:',
            ])
            ->addDirective(Directive::OBJECT, 'none');
    }

    protected function addDirectivesForBootstrap(): self
    {
        return $this
            ->addDirective(Directive::FONT, ['*.bootstrapcdn.com'])
            ->addDirective(Directive::SCRIPT, ['*.bootstrapcdn.com'])
            ->addDirective(Directive::STYLE, ['*.bootstrapcdn.com']);
    }

    protected function addDirectivesForCarbon(): self
    {
        return $this->addDirective(Directive::SCRIPT, [
            'srv.carbonads.net',
            'script.carbonads.com',
            'cdn.carbonads.com',
        ]);
    }

    protected function addDirectivesForGoogleFonts(): self
    {
        return $this
            ->addDirective(Directive::FONT, 'fonts.gstatic.com')
            ->addDirective(Directive::SCRIPT, 'fonts.googleapis.com')
            ->addDirective(Directive::STYLE, 'fonts.googleapis.com');
    }

    protected function addDirectivesForGoogleAnalytics(): self
    {
        return $this->addDirective(Directive::SCRIPT, '*.google-analytics.com');
    }

    protected function addDirectivesForGoogleTagManager(): self
    {
        return $this->addDirective(Directive::SCRIPT, '*.googletagmanager.com');
    }

    protected function addDirectivesForTwitter(): self
    {
        return $this
            ->addDirective(Directive::SCRIPT, [
                'platform.twitter.com',
                '*.twimg.com',
            ])
            ->addDirective(Directive::STYLE, [
                'platform.twitter.com',
            ])
            ->addDirective(Directive::FRAME, [
                'platform.twitter.com',
                'syndication.twitter.com',
            ])
            ->addDirective(Directive::FORM_ACTION, [
                'platform.twitter.com',
                'syndication.twitter.com',
            ]);
    }

    protected function addDirectivesForYouTube(): self
    {
        return $this->addDirective(Directive::FRAME, '*.youtube.com');
    }
}

Go ahead an inspect the Content-Security-Policy header of this page to see the result of the policy above.

In closing

The package has some more features, including support for nonces, and reporting. To learn more head over to the readme of the package on GitHub. Be sure to also read the aforementioned Mozilla docs on CSP.

To test out how solid the headers of your site are security-wise check out https://securityheaders.io/ where you can run an a test. This blog gets an A+ score, which I'm proud of.

a plus score

Laravel-csp is not the first package my team has made. On our company website you'll find lists of each Laravel, PHP and JavaScript packages we've made before. I'm pretty sure you'll find something useful for your next project.

Stay up to date with all things Laravel, PHP, and JavaScript.

You can follow me on these platforms:

On all these platforms, regularly share programming tips, and what I myself have learned in ongoing projects.

Every month I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Comments

Tim Gavin avatar

Unfortunately https://freek.dev is now getting a "D" at https://securityheaders.com :(

Comments powered by Laravel Comments
Want to join the conversation? Log in or create an account to post a comment.