Security Tip: Getting Started with Content Security Policies

[Tip#47] Setting up a CSP doesn't have to be a daunting task! Let's take a look at a tips for getting started with CSPs, without breaking anything!

Security Tip: Getting Started with Content Security Policies
ℹ️
This is part of my series on the Top 10 Security Issues discovered during my Laravel Security Audits, as of April 2023.

#1 → Exposed API Keys & Passwords
#2 → Missing Authorisation
#3 → Missing Content Security Policy (CSP)
#4 → Missing Security Headers
#5 → Insecure Function Use
#6 → Outdated & Vulnerable Dependencies
#7 → Cross-Site Scripting (XSS)
#8 → Insufficient Rate Limiting
#9 → Missing Subresource Integrity (SRI)
#10 → Insufficient Input Validation & Mass-Assignment Vulnerabilities

I talk about Content Security Policies (CSPs) a fair bit, but if you haven’t heard of them before, CSPs are a way to instruct the web browser that resources your app is allowed to interact with. It was designed as a layer of defence to prevent Cross-Site Scripting (XSS) and clickjacking attacks, and consists of a set of directives and rules which define the allowed behaviour. When fully configured, a CSP can prevent any unwanted scripts, styles, fonts, forms, etc, from being executed - locking a site down completely.

As an example, this is the CSP from practicallaravelsecurity.app:

Content-Security-Policy: 
    report-uri      https://valorin.report-uri.com/r/d/csp/enforce ; 
    default-src     'none' ; 
    connect-src     'self' https://cdn.usefathom.com ;
    font-src        'none' ;
    frame-src       https://practicallaravelsecurity.app https://*.practicallaravelsecurity.dev ;
    img-src         'self' data: https://cdn.usefathom.com ; 
    manifest-src    'self' ; 
    script-src      'report-sample' 'self' https://cdn.usefathom.com 'nonce-cH48M...' ; 
    style-src       'self' 'nonce-cH48M...' ; 
    form-action     'self' ; 
    frame-ancestors 'none'

There are two modes you can implement a CSP in:

  1. Content-Security-Policy → Blocks and reports all policy violations.
  2. Content-Security-Policy-Report-Only → Reports all policy violations only.

You can use either or both modes on the same site, a making CSPs incredibly flexible. You can deploy a flexible blocking CSP alongside a strict Report-Only CSP, to test out new directives without breaking anything, and still retain the protection of your flexible CSP.

I covered CSPs in one of my early In Depth articles, so go check that out to learn more about how CSPs work and how to configure them:

In Depth: Content Security Policy
[InDepth#7] Content Security Policies are an incredibly powerful security feature built into the browser, and as it turns out, they are also pretty easy use.

Implementing CSPs in Laravel

My recommended approach is to use a simple middleware class and a config file to configure and load your CSP. This gives you full control over the directives and makes it easy to add in support for nonces, etc, without adding in extra complexity.

You can use my middleware in this Gist: https://gist.github.com/valorin/d4cb9daa190fdee90603efaa8cbc5886.

If you’re after something with a lot more features or wish to define your CSPs within classes, Spatie have a CSP package: https://github.com/spatie/laravel-csp.

Reporting Tools

My favourite reporting tool for CSPs (and many other security things) is Report URI. Report URI have a free tier, which is great for getting started. I have a paid account, which allows me to monitor a number of sites easily.

If you’re a user of Honeybadger, they can receive CSP reports as well, which is handy for handling CSP violations alongside app errors, etc.

CSPs in New Projects

CSPs are relatively easy to add into new projects. You can build the directives as you add in dependencies, test then in local dev, and roll them out, knowing that there will be nothing unexpected you’ve missed.

Just start with a basic policy that blocks everything, and start allowing things as required:

Content-Security-Policy: default-src 'none'; form-action 'none'; frame-ancestors 'none'; report-uri <reporting_url>

I recommend configuring your CSP middleware to run Report-Only mode in local dev, and non-report-only in production. This allows you to track violations in local dev, without it breaking things when you’re testing new features. It also prevents Laravel’s Error page from breaking, since it uses a bunch of inline scripts and styles.

CSPs in Existing Projects

Adding CSPs to existing projects can seem like a daunting task, but there are a couple of steps you can take to make it relatively painless.

First, sign up for Report URI and configure their CSP Wizard for your site. The wizard receives all violations, reduces them down into the required directives, and them helps you build your CSP header.

Secondly, deploy a Report-Only header to your site, blocking everything:

Content-Security-Policy: default-src 'none'; form-action 'none'; frame-ancestors 'none'; report-uri https://<subdomain>.report-uri.com/r/d/csp/wizard

Browse around your app and wait for your users to use it.

You’ll see the reports landing in the wizard, where you can add directives to the compiled policy, and block unwanted directives. Once you’re starting to build your policy, you can periodically update the policy in your app to reduce the number of violations hitting the wizard.

When you’re happy, you can disable the wizard and do one of two things:

  1. Stay on Report-Only mode.
    Report-Only mode gives you complete awareness of what’s happening in your app, without the risk of breaking anything. This is a fantastic option for complex or legacy apps. This is what I run on WordPress sites, do to their unique nature.
  2. Switch off Report-Only mode.
    If you’re confident in your policy, switching off report-only mode will block any violations, and keep your site protected. This is how I run a CSP on practicallaravelsecurity.app

If you liked this Security Tip, please share it (and Securing Laravel!) with your Laravel friends, colleagues, and enemies. Don't forget to tag me I'll give it a retweet! The more folks we have learning about security, the more secure code will be written, and the safer the whole community and our users will be.