DEV Community

Cover image for Who Needs CSP?
Michael Mathews
Michael Mathews

Posted on

Who Needs CSP?

You've probably heard about Content Security Policy (CSP) in the context of web security. But what is CSP for, and is it appropriate for your project?

What is Content Security Policy (CSP)?

At its core, CSP is a set of security practices and a standard that can be used to tell browsers which resources your website will be allowed to load and execute. Think of it as a security guard standing at the front door of your web page—it only lets scripts, styles, and other resources inside if they are on the guest list.

The way you tell the web browser which resources are considered safe to allow in is with a Content-Security-Policy HTTP header. An example CSP configuration heading might look like this:

Content-Security-Policy: default-src 'self'; script-src 'self' https://apis.google.com
Enter fullscreen mode Exit fullscreen mode

This configuration tells the browser: "By default, only load resources from my own domain (self), and any scripts must come from my domain or Google's API server." It also blocks any inline CSS and JS from running.

The Security Problem CSP Solves

CSP is primarily intended to protect against XSS (Cross-Site Scripting) attacks. This class of attack occurs when a hacker injects malicious code into your web page.

For example, without CSP, if a baddie injects <script>stealAllYourUserData()</script> into your web page's source code, the browser will execute it, the same as if you wrote that code yourself. But with CSP, the browser must first check whether the script originated from a pre-approved source; if not, it blocks it, preventing the hacker's code from running.

XSS Examples

There are lots of ways that a user could (intentionally or not) inject JS or CSS into an unprotected webpage. XSS is a possibility in any place where a user can submit content that is later displayed on your page. Some examples include:

  • Comment sections, forum posts, and reviews where HTML or markdown is not filtered effectively/escaped.
  • User profiles with custom bios, display names, or avatars that allow HTML/JS.
  • File uploads where SVGs (which can contain script) or text files are served as HTML.

CSP Benefits and Trade-offs

You may say, "But if I'm already super careful about not allowing arbitrary user code to appear in my web page, why do I need CSP as yet another layer of security?" It's a fair question.

Think of CSP like seatbelts in cars. Even if you drive carefully (secure coding), maintain your vehicle (input validation), and follow traffic rules (authentication), you still wear a seatbelt (CSP) because accidents sometimes happen for reasons beyond your control. Using CSP adds another safety net in case something goes wrong unexpectedly.

Do You Need CSP for Your Website

Likely yes if your website:

  • Handles sensitive data, such as financial, medical, and personal information
  • Allows user-generated content, for example, comments, posts, rich text editors
  • Uses many third-party scripts, such as analytics, ads, widgets
  • Is a high-value target, such as e-commerce, banking, social platform

Maybe not if your website:

  • Is a simple static site, for example, a static blog, portfolio, or documentation site
  • Has no user-generated content, never displays user comments, for example
  • Has no third-party included content, for example, analytics, font, or ad services
  • Are building internal tools with limited exposure
  • Are early in development, such as a prototype, and you intend to reassess your vulnerabilities before moving to production

But honestly, there aren't many good reasons not to take security as seriously as possible these days. My advice is to be smart and be safe!

Getting Started

The basic goal of CSP is to use the HTTP header to authorise the resources your web page is allowed to use. So you will need to configure your web server to add a Content-Security-Policy header to your HTML response.

1. Start Simply

The following policy allows the page to load and execute resources (including scripts and styles) only from the same origin (domain) as the HTML document. It blocks inline scripts and styles unless you explicitly relax it with additional unsafe directives such as script-src 'unsafe-inline' or style-src 'unsafe-inline'.

Content-Security-Policy: default-src 'self'
Enter fullscreen mode Exit fullscreen mode

It's (very) possible to accidentally exclude legitimate resources with a CSP header, so be sure to thoroughly test your site before publishing security restrictions.

2. Use Report-Only Mode First

Deploy in monitoring mode to see what would break:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports
Enter fullscreen mode Exit fullscreen mode

3. Gradually Add More Complex Restrictions

Once you've identified and fixed any initial issues, switch to enforcement mode and slowly add more complex rules.

4. Focus on the Biggest Risks

Prioritize script-src and style-src directives first since these are where most XSS vulnerabilities happen.

A Basic Starter CSP

Content-Security-Policy: 
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self'
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Allows resources only from your own domain ('self')
  • Blocks any inline scripts and styles (which could be injected via XSS attacks)
  • Allows images from your domain, data URLs, and any HTTPS source
  • Prevents connections to any external APIs except your own

Handling Inline CSS/JS

Adding default-src 'self' not only blocks any external domains—such as popular CDNs, analytics, fonts, etc.—it also blocks all inline CSS and JS code. If you need to allow inline CSS or JS, there are two possible approaches: hashes (best for static sites) or nonces (for dynamically generated sites).

💁‍♂️ Technically, you could add script-src 'unsafe-inline' to, for example, allow any inline JS to execute, but that would completely defeat the whole purpose of CSP, so obviously, don't do that.

Option 1: Using Hashes (Best for Static Sites)

First, identify your critical inline code:

<!-- Critical above-the-fold CSS -->
<style>
.hero { background: #fff; color: #333; }
.button { padding: 1rem 2rem; }
/* ... more critical styles */
</style>

<!-- Essential initialization script -->
<script>
window.APP_CONFIG = { apiUrl: '/api', theme: 'dark' };
initializeApp();
</script>
Enter fullscreen mode Exit fullscreen mode

Calculate the SHA-256 hashes for each inline code and include them in your CSP header:

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' 'sha256-abc123...';
  style-src 'self' 'sha256-def456...';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self'
Enter fullscreen mode Exit fullscreen mode

Remember, once you generate a hash for a string of CSS or JS, changing even a single character of the code requires you to regenerate the hash and update the header again. If you rarely modify inline code or have an automated system to handle it for you, this might not be a problem, but it does introduce a footgun that might surprise someone (in a bad way) later on when they "just need to make a quick change" to your code.

How to generate hashes

Assuming you have decided to add a hash to your CSP header to authorize some inline JS, you can use any tool capable of making sha256 hashes of strings, such as openssl.

# For a script: <script>alert('Hello');</script>
echo -n "alert('Hello');" | openssl dgst -sha256 -binary | openssl base64
Enter fullscreen mode Exit fullscreen mode

This produces the hash gKHd+pSZOJ3MwBsFalomyNobAcinjJ44ArqbIKlcniQ=.

You would then add the following to your CSP header to allow the browser to run it:

script-src 'self' 'sha256-gKHd+pSZOJ3MwBsFalomyNobAcinjJ44ArqbIKlcniQ=';
Enter fullscreen mode Exit fullscreen mode

Ideally, this would be set up to happen automatically during your build process.

Option 2: Using Nonces (Dynamic Sites)

Using a nonce makes more sense if you have blocks of inline code you want to allow, but that may change frequently, are dynamic, or differ on every page, and generating a hash for every page is too onerous.

Instead, you could configure your server to generate a unique nonce, any hard-to-guess string that is different for each request. Your server would add that value to each <style> or <script> tag you want to safelist (regardless of its contents), and include the nonce in the HTTP headers.

<style nonce="cryptoR4nd0m123">
  /* Any CSS in here to be considered safe */
</style>

<script nonce="cryptoR4nd0m123">
  // Any JS in here to be considered safe
</script>
Enter fullscreen mode Exit fullscreen mode
Content-Security-Policy: 
  default-src 'self';
  script-src 'self' 'nonce-cryptoR4nd0m123';
  style-src 'self' 'nonce-cryptoR4nd0m123';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self'
Enter fullscreen mode Exit fullscreen mode

Remember, a nonce is not just hard to guess (like a password); it must also be unique for every single page view. Otherwise, a hacker could just hit "view source", copy whatever nonce they find, and inject their baddie code with that same nonce value. By making the value unique per view, knowing an already-used nonce is useless to the hacker.

The nonce approach is arguably more straightforward and performant than hashes because there is no hash generation involved, but having to modify the <script> or <style> with a unique nonce attribute for every page view will destroy any server-side caching strategy you may have in place. It also means you cannot serve static web pages at all, since, by definition, a nonce is never repeated from one page view to the next.

When you need server-side caching or static HTML, the hash method is your better option.

More Examples

Basic Protection

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'
Enter fullscreen mode Exit fullscreen mode

Add Some External Resources and Inline Resources

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' 'sha256-abc123...' https://www.google-analytics.com https://cdnjs.cloudflare.com;
  style-src 'self' 'sha256-def456...' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: https: https://www.google-analytics.com;
  connect-src 'self' https://api.example.com
Enter fullscreen mode Exit fullscreen mode

Move to Stricter Protection

Content-Security-Policy: 
  default-src 'none';
  script-src 'self' 'sha256-abc123...';
  style-src 'self' 'sha256-def456...';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none'
Enter fullscreen mode Exit fullscreen mode

Development vs Production

You will likely find that you need different security policies when developing than when in production.

Development (more permissive):

Content-Security-Policy-Report-Only: 
  default-src 'self';
  script-src 'self' 'unsafe-eval';  # For hot reloading
  style-src 'self' 'unsafe-inline'; # For development convenience
  report-uri /csp-reports
Enter fullscreen mode Exit fullscreen mode

Production (more restrictive):

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' 'sha256-abc123...';
  style-src 'self' 'sha256-def456...';
  img-src 'self' data: https:;
  font-src 'self'
Enter fullscreen mode Exit fullscreen mode

Example: Adding CSP to an AWS CloudFront Response Headers Policy

This is a basic, straightforward approach if you are using CloudFront content distribution caching.

Step 1: Create a Response Headers Policy

  1. AWS Console → CloudFront → Policies → Response headers
  2. Click "Create response headers policy"
  3. Configure a basic CSP that says only CSS and JS from files on the same domain are allowed:
  Name: my-awesome-csp-policy
  Headers:
    - Content-Security-Policy: 
        Value: "default-src 'self'; script-src 'self'; style-src 'self'"
        Override: true
Enter fullscreen mode Exit fullscreen mode

Note that this will tell the browser not to run any inline CSS or JS, and to allow such resources only from files hosted on the same domain.

Step 2: Attach Policy to Specific Cache Behaviors

  1. CloudFront → Distributions → Your Distribution
  2. Go to the Behaviors tab
  3. Edit the specific path pattern (e.g., /*, /secure/*, or /checkout)
  4. In Response headers policy, select your CSP policy, "my-awesome-csp-policy" in this example
  5. Save changes

Result: Only requests matching that path pattern will get the CSP header.

Is CSP a Silver Bullet to Prevent All XSS?

Nope. There are still plenty of ways a user could inject code to run in your web page if you aren't careful. For example, if you assume that strings in a browser cookie, in the url, or from an API endpoint are "safe" and evaluate them or write them to the document with JavaScript, congratulations: you've just opened a door to XSS that CSP can't protect you from.

The point is, adding CSP defences doesn't mean you can stop thinking about XSS. It's still a hard problem, and it's still your responsibility.

Summary

Determine whether your specific use case requires CSP and whether the trade-offs are justified. Consider CSP another layer of your website defence strategy; don't neglect other methods to prevent unauthorized content injection. For your first effort using CSP, begin with a minimal configuration that blocks inline scripts and styles. If you need inline code (for performance reasons, for example), use hashes for specific, verified content. This approach will give you 80% of the security benefits at 20% of the complexity.


photo credit: Rulo Davila

Top comments (1)

Collapse
 
ingosteinke profile image
Ingo Steinke, web developer • Edited

CSP, in my personal experience, tries to fix real problems at the wrong place, creating a lot of new problems, that's why nobody dares to use it. I 100% agree to your "do you need it?" section. Please, use CSP (and every possible security vector) on a professional platform that interacts with user input. But if you think you need CSP for a JAM stack business profile page, than you might have other problems.