Laravel Content Security
Policy Guide: What It Is
and How to Enable It

stackhawk

StackHawk|September 17, 2021

Today, we'll look at a broader and more versatile tool to increase the security of web applications: the Laravel content security policy.

The strength and versatility of modern browsers that enable powerful web applications can turn into a security nightmare. We previously covered security issues like cross-site-scripting (XSS), SQL injections, and path traversals. The ability to run custom or third-party code leads to unwanted behavior and data leaks. Browsers have a same-origin policy that prevents the execution of some third-party content by default, and developers can use cross-origin resource sharing (CORS) to control this behavior. Today, we'll look at a broader and more versatile tool to increase the security of web applications: the content security policy (CSP). We'll first discuss CSPs in general and then see how we can add them to PHP applications that use the Laravel framework.

Ready to Test Your App

What Is CSP?

A content security policy is a set of rules or directives that allow or deny the inclusion, display, and execution of specific types of content on a web page.

Websites send their CSPs as custom HTTP headers or using a <meta> tag in the <head> of the HTML page. While the <meta> tag works, the HTTP header is the preferred solution because of the clear separation between content and metadata. The browser reads the policy, intercepts the content that the page tries to load, and blocks or reports it if it violates the specified rules.

Let's look at an example. In its CSP, a website might indicate that all scripts must come from the same domain as the website. This ensures that maliciously injected code cannot load and execute third-party JavaScript. However, the same site may allow other content from remote hostnames because it wants to use Google Fonts via a content delivery network (CDN). It might also have a forum section where users can hotlink images on the web. A browser should load the website and let the fonts and pictures through but block unwanted scripts.

Do You Need a CSP for Your Website?

Here's the first answer, but promise me you'll continue reading afterward, okay? No, you don't strictly and technically need a CSP. Your website will work fine without it. Unlike CORS, which extends default limitations in browsers, the default behavior for CSP rules is "allow all." It has a historical background because most browser capabilities are much older than CSP and because legacy websites would break if browsers suddenly took them away.

However, if you know even a bit about IT security, you know that "allow all" is the worst possible policy. So as with many security policies, it can be a bit of a hassle to set them up, and you may feel you're doing well without them, but in the long run, the effort pays off. With a CSP, your website will be at a much lower risk for injection-style attacks. So my second answer is yes, you should have a CSP.

How Do I Add a CSP?

There are two parts involved in improving your website with a content security policy. One part is designing the policy and deciding which rules you need. Those rules depend on the content that you already have or want to have on your website. The second is deploying the policy. For best results, you may need to modify other portions of your website code as well, but we'll get to that in a bit.

Specifying Directives

It's crucial to get the rules right. Let's go back to the example above. The website decided to block all scripts. Now the developers want to allow users to sign in to the site with their Facebook accounts. They add the Facebook SDK to their code and wonder why it doesn't work. The reason is that they blocked all third-party scripts, so the browser won't load an SDK from Facebook's domain. Of course, it's not necessary to allow all remote hostnames. A CSP is flexible enough that developers can only allow '*.facebook.com' and '*.facebook.net' as valid origins and still block scripts from any other vendor.

By the way, this specific allowance is also beneficial when following privacy regulations like the GDPR because you can explicitly name all the "data processors" that may receive personal data from your users.

Deploying The Policy

A CSP is just an HTTP header. To be exact, it's the Content-Security-Policy header. There are various ways to deploy such a header. You could change your webserver configuration or (for Apache) add an .htaccess file to rewrite the response automatically. If there's a reverse proxy or CDN in front of your Laravel application, you can add the header there. Still, I recommend configuring your CSP in the Laravel application itself. One reason is because that keeps it near the code where you know the requirements best. You can change and extend your policy with additional rules whenever necessary, as I illustrated with the example above. It also works everywhere regardless of deployment, even on your development computer. That means you can test extensively to make sure nothing breaks.

There are two modes for adding a CSP. The standard Content-Security-Policy header instructs the browser to block all content that violates the policy. The alternate Content-Security-Policy-Report-Only header doesn't block anything. Still, it shows warnings in the browser's developer tools console that indicate what would be blocked if you armed the policy. For both modes, it's also possible to send reports to a remote URL.

What About My Inline Scripts and Styles?

One caveat about deploying a content security policy is that its default behavior blocks all inline scripts and styles. While this is great for blocking unwanted third-party insertions, it also blocks desired code from running. If you have all your JavaScript and CSS code in individual files cleanly separated from your HTML code and Blade templates, you're good to go. Many applications, however, have <script> blocks with inline JavaScript code or <style> blocks with specific CSS instructions inside their HTML code.

The easiest way to solve the problem is to allow inline styles and scripts. There's a CSP rule for that. However, if your desired inline script tags can execute, so can the maliciously inserted script tags. What should you do then?

CSP has two solutions: hashes and nonces. For dynamic applications like Laravel projects, nonces are the way to go. I'll show you how to add them soon.

How Do I Add a CSP in Laravel?

You could certainly add the header manually in your route controller implementations or write custom middleware, but you don't have to. An open-source library from Spatie (a Belgian company specializing in Laravel) helps set up CSP for Laravel applications. To install the library, enter the following commands in your console:

composer require spatie/laravel-csp
php artisan vendor:publish --provider="Spatie\Csp\CspServiceProvider" --tag="config"

With the Laravel CSP library, you don't need to generate your policy as an arbitrary string with new syntax to learn. Instead, policies are PHP classes that extend the Spatie\Csp\Policies\Policy class. The library also has a "Basic" policy with reasonable defaults, such as allowing all types of content when loaded from the same domain and supporting nonces for inline scripts. You can leverage it and create a custom policy by extending Spatie\Csp\Policies\Basic. If you don't need different directives, the "Basic" policy is active out of the box after installing the library. The only thing left for you to do is enable the CSP middleware.

As with all Laravel middleware, you can enable it globally by adding it to the $middleware or $middlewareGroups in the App\Http\Kernel class for your application:

protected $middlewareGroups = [
'web' => [
...
\Spatie\Csp\AddCspHeaders::class,
],

The most sensible thing is probably adding it to the 'web' group in $middlewareGroups. You don't need a CSP for the 'api' group since an API client is not a browser and likely doesn't understand the policy anyway. An alternate option for deploying the policy only on specific routes is adding them to the route definition:

Route::get('my-page', 'MyController')->middleware(Spatie\Csp\AddCspHeaders::class);

Great, you have now CSP-enabled your application! You can test it by doing something forbidden, such as adding a third-party element to your HTML code. When you load the page, the browser should block the element.

How Can I Add Custom Directives?

Let's go back to the example of the web application that needed Facebook integration. The "Basic" policy wouldn't allow it, so let's create a new file app/ContentPolicy.php with the following content:

<?php
namespace App;
use Spatie\Csp\Directive;
use Spatie\Csp\Policies\Basic;
class ContentPolicy extends Basic
{
public function configure()
{
parent::configure();

$this->addDirective(Directive::DEFAULT, '*.facebook.net');
$this->addDirective(Directive::DEFAULT, '*.facebook.com');
}
}

As you can see in the example, we're allowing '*.facebook.net' and '*.facebook.com'.
Now we need to tell Laravel to use our custom content policy instead of the preconfigured basic policy. To do so, edit the config/csp.php file and change the 'policy' entry to point to your custom class like this:

<?php
return [
/*
* A policy will determine which CSP headers will be set. A valid CSP policy is
* any class that extends `Spatie\Csp\Policies\Policy`
*/
//'policy' => Spatie\Csp\Policies\Basic::class,
'policy' => App\ContentPolicy::class,
...

While we're in this configuration file, let's quickly look at some other things we can set up here:

  • By adding our class as 'report_only_policy' instead of 'policy', we can instruct the browser to report policy violations but not enforce them.

  • Through 'report_uri', we can implement remote reporting.

  • With 'enabled', we can quickly turn CSP on or off.

What Are the Kinds of Directives I Can Specify?

Let's take a more general look at adding additional directives:

  • You use the addDirective() method in your policy file to add additional rules to your policy. It takes two parameters.

  • The first parameter specifies the type of content or fetch action in the browser that the directive handles. With Directive::DEFAULT, it applies to all kinds of content. Using Directive::IMG, for example, only applies to fetching images. Other commonly use directives are Directive::MEDIA (for embedded audio and video content), Directive::SCRIPT (for scripts), and Directive::STYLE (for stylesheets).

  • The second parameter specifies the origin of the content, which can be a domain with additional wildcards. Alternatively, you can select a keyword. Keyword::SELF refers to the source your page loads from, and Keyword::NONE disables this type of content for any origin.

How Can I Add Nonces to My HTML Code?

As mentioned earlier, the default CSP behavior is to disable inline scripts and styles. While it's possible to allow them globally, you should generally rely on hashes or nonces as the safer solution.

A nonce is a unique number that changes for every request. The browser only executes scripts that have the correct nonce. The default configuration of the Laravel CSP plugin generates nonces and adds them to the Content-Security-Policy header. The only thing left for you to do is to add them to your HTML output.

In a Laravel project, you typically use Blade templates for your HTML. Search for all instances of <script or <style in your templates and modify them like this:

<style nonce="{{ csp_nonce() }}">
...
</style>
<script nonce="{{ csp_nonce() }}">
...
</script>

That's all you need to do. The csp_nonce() helper function takes care of everything. You never have to deal with nonces directly.

Find and Fix Application Security Vulnerabilities with Automated Testing

To Summarize

A content security policy prevents many injection-style attacks and should be a staple of a secure web application. A CSP is an HTTP header with fine-grained directives that tells the browser what kinds of content it may load for the page from which origin. For Laravel applications, a plugin library allows adding CSPs with PHP code. The library also handles nonces that secure inline scripts and styles to simplify the deployment of CSP.

This post was written by Lukas Rosenstock. Lukas is an independent PHP and web developer turned API consultant and technical writer. He works with clients of all sizes to design, implement, and document great APIs. His focus is on creating integrations and mashups that highlight the values of APIs, and educate developers about their potential.


StackHawk  |  September 17, 2021