Freek Van der Herten
Posted on January 2, 2020
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.
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.
Posted on January 2, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.