ToolPal
Lock and security concept

CSP Generator: Content Security Policy Without the Headaches

πŸ“· Pixabay / Pexels

CSP Generator: Content Security Policy Without the Headaches

Content Security Policy protects against XSS and injection attacks, but the syntax is brutal to hand-write. Learn CSP directives, common pitfalls, and how to generate headers without the pain.

April 14, 202612 min read

The first time I encountered a Content Security Policy violation in production, I had no idea what was happening. The site loaded, most things worked, but a third-party widget was silently dead. The browser console had a wall of red CSP errors that I hadn't seen locally. It took an embarrassing amount of time to figure out that someone had added a new external script without updating the policy. The widget vendor had no idea either β€” they just said "it works on other sites."

That experience β€” and a few like it β€” is why I actually care about CSP now. It's not the most exciting security topic, but it's one of the most practical things you can add to a production website to stop a real category of attacks.

The XSS Problem

Cross-site scripting (XSS) is still one of the most common web vulnerabilities, despite being well understood for decades. The attack is conceptually simple: if an attacker can get malicious JavaScript to execute in the context of your page, they can steal session tokens, read sensitive data, redirect users, or silently make API calls as the logged-in user.

The classic vector is user-generated content β€” if you allow users to post comments or profile descriptions, and you render that content without proper escaping, an attacker can embed script tags. But XSS can also come from compromised CDN scripts, browser extensions injecting code, or inline event handlers.

Proper output escaping in your templates is the first line of defense. CSP is the second line β€” a browser-enforced backstop that says "even if malicious JavaScript somehow gets into this page, the browser won't execute it."

What CSP Actually Does

CSP is delivered as an HTTP response header:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com;

When a browser sees this header, it treats it as a whitelist. Every resource the page tries to load β€” scripts, stylesheets, images, fonts, API requests, iframes, WebSocket connections β€” gets checked against the policy. Anything not explicitly allowed is blocked, and the browser logs a violation.

The key thing to understand: CSP is enforced by the browser, not by your server. Your server just sends the header. The browser does the actual enforcement. This means CSP works even if an attacker has already injected content into your HTML β€” the browser will refuse to execute the malicious script because it wasn't in the approved sources.

This is also why CSP can't protect against server-side attacks. It's a client-side control. If an attacker can directly access your database or API, CSP doesn't help. But for the specific threat of malicious JavaScript executing in a user's browser, CSP is genuinely effective.

The Core Directives

CSP has a lot of directives, but most of the time you only need to care about a handful. Here's what actually matters for a typical web application:

default-src

This is the fallback for any resource type that doesn't have its own specific directive. It's good practice to start with a restrictive default-src and then open up only what you need for specific types.

default-src 'self'

This single directive tells the browser: by default, only load resources from my own origin. Everything else falls back to this if there's no more specific directive for that resource type.

script-src

This controls where JavaScript can be loaded from β€” and it's the most important one for XSS protection. If you allow 'unsafe-inline' here, you've largely undermined the XSS protection that CSP provides.

script-src 'self' https://cdn.jsdelivr.net

style-src

Similar to script-src but for CSS. Inline styles (style="" attributes) count as 'unsafe-inline' here. Many sites need this because CSS-in-JS libraries like styled-components inject inline styles.

style-src 'self' 'unsafe-inline' https://fonts.googleapis.com

img-src

Controls where images can be loaded from. This is usually more permissive than script-src since images can't execute code.

img-src 'self' data: https://images.cdn.example.com

The data: source allows data URIs, which you need if you're embedding images as base64 inline β€” common in email-style applications.

connect-src

Controls where fetch(), XMLHttpRequest, WebSocket, and EventSource can connect to. Critical for SPAs that make API calls.

connect-src 'self' https://api.example.com wss://realtime.example.com

font-src

Where web fonts can be loaded from. Google Fonts needs to be listed here if you're loading fonts externally.

font-src 'self' https://fonts.gstatic.com

frame-src and frame-ancestors

frame-src controls which URLs your page can embed in iframes. frame-ancestors controls which pages can embed yours β€” it's the modern replacement for the X-Frame-Options header and more flexible.

frame-ancestors 'none'

That 'none' tells browsers that nothing is allowed to embed this page in a frame, which prevents clickjacking attacks.

The Confusing Quotes

This is one of the most common sources of CSP errors: keyword values must be wrapped in single quotes in the header value, but domain names must not be.

/* Correct */
script-src 'self' 'unsafe-inline' https://cdn.example.com

/* Wrong β€” 'self' without quotes won't work */
script-src self https://cdn.example.com

/* Wrong β€” domain in quotes won't match */
script-src 'self' 'https://cdn.example.com'

Keywords like 'self', 'none', 'unsafe-inline', 'unsafe-eval', 'nonce-...' all need single quotes. Domain names and URL patterns do not.

This is a spec design decision that I think caused enormous amounts of initial confusion. The quotes are part of the syntax, not quoting in the conventional sense. But once you know the rule, it's mechanical: keywords get quoted, URLs don't.

unsafe-inline: The Necessary Evil

In an ideal world, you'd never use 'unsafe-inline'. It effectively says "inline scripts/styles are allowed," which means an injected <script>alert(1)</script> would execute.

But the real world is messy. A lot of older codebases use onclick="" attributes. A lot of UI frameworks inject styles at runtime. Some third-party integrations require inline scripts. When you're adding CSP to an existing application, you often need 'unsafe-inline' as a temporary measure while you clean things up.

The pragmatic advice: if you must use 'unsafe-inline', accept it as a starting point and work toward removing it. Adding CSP without 'unsafe-inline' for script-src is much better than no CSP. Adding 'unsafe-inline' temporarily to get a working policy in place is still progress.

For style-src, 'unsafe-inline' is less dangerous β€” stylesheets can't directly steal data or make requests. CSS injection attacks exist but they're significantly harder to exploit than script injection. So being permissive with style-src is a reasonable trade-off if it means you can be strict with script-src.

Nonces: The Proper Solution for Inline Scripts

If you genuinely need inline scripts but don't want to use 'unsafe-inline', nonces are the answer.

A nonce is a random, single-use value that you generate server-side for each request. You include it in the CSP header and on each inline script tag:

<!-- HTTP header -->
Content-Security-Policy: script-src 'nonce-abc123XYZ'

<!-- In the HTML -->
<script nonce="abc123XYZ">
  // This inline script is allowed because it has the matching nonce
  console.log('hello');
</script>

The nonce must be cryptographically random and unique per request β€” if an attacker can predict the nonce, the protection is useless. This approach requires server-side rendering (you can't use nonces effectively with a purely static site unless you regenerate on every request).

Next.js has built-in middleware support for generating CSP nonces, which is convenient if you're on that stack.

Hashes: For Static Inline Scripts

If your inline scripts are static (they don't change between requests), you can use hashes instead of nonces:

script-src 'sha256-xyz...=='

The hash is a base64-encoded SHA hash of the script content. The browser calculates the hash of each inline script and compares it against the allowed hashes in the policy. If you change the script, the hash no longer matches β€” which is actually a feature, since it forces you to explicitly update the policy when you update the script.

Generating these hashes by hand is tedious. This is one of the things the CSP Generator handles for you.

The 8 Directives You Actually Need

For most production web applications, this set covers about 90% of cases:

  1. default-src 'self' β€” restrictive fallback
  2. script-src β€” where JS loads from (no unsafe-inline if possible)
  3. style-src β€” where CSS loads from
  4. img-src β€” images, plus data: if needed
  5. connect-src β€” API endpoints, WebSocket servers
  6. font-src β€” web fonts
  7. frame-ancestors 'none' β€” prevents clickjacking
  8. upgrade-insecure-requests β€” forces HTTPS for all subresource loads

That last one is easy wins β€” it converts http:// requests to https:// automatically, which helps if you have any legacy HTTP URLs buried in old content.

Report-Only Mode: Your Best Friend

Here's the number one mistake people make when deploying CSP: going straight to enforcement on a production site they've never tested CSP on before.

The result is predictable. Something breaks. Sometimes it's a major feature, sometimes it's a third-party widget, sometimes it's a monitoring script. Users notice before you do.

The correct approach is to use Content-Security-Policy-Report-Only first:

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://cdn.example.com; report-uri /csp-reports

This header tells the browser to check resources against the policy and report violations, but not to actually block anything. Your site keeps working normally, and you get a stream of violation reports telling you exactly what would have been blocked.

Set up a simple endpoint to collect and log these reports, run it for a few days while actual users use your site, then analyze what showed up. You'll discover third-party scripts you forgot about, CDN domains that weren't in your list, and data URIs from plugins. Fix those in the policy, then switch from Report-Only to the enforcing header.

CSP for SPAs vs Server-Rendered Pages

Single-page applications and server-rendered pages have genuinely different CSP challenges.

Server-rendered pages are easier to secure because the HTML is generated fresh on each request. You can inject nonces into both the header and the script tags dynamically. Tools like Helmet.js (Express), Django-CSP, or Next.js middleware make this straightforward.

SPAs are harder for a few reasons:

First, many SPA frameworks rely on inline event handlers or inject styles at runtime. React and Vue are generally fine, but older widget libraries and jQuery plugins often use inline patterns.

Second, SPAs make lots of fetch() calls, so you need a comprehensive connect-src that includes all your API domains, analytics endpoints, error reporting services, etc.

Third, static SPAs hosted on CDNs can't easily use nonces since there's no server generating each response. You're stuck with either 'unsafe-inline' or hashes (which require rebuilding every time you change an inline script).

For Next.js specifically, the middleware.ts file is the right place to set CSP headers:

// middleware.ts
import { NextResponse } from 'next/server';

export function middleware() {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
  const csp = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}';
    style-src 'self' 'unsafe-inline';
    img-src 'self' data: https:;
  `.replace(/\s{2,}/g, ' ').trim();

  const response = NextResponse.next();
  response.headers.set('Content-Security-Policy', csp);
  return response;
}

This runs on every request and generates a fresh nonce, which Next.js can then pass to your page components through headers.

Common Mistakes That Break Production

Blocking your analytics. Google Analytics, Plausible, Fathom β€” they all need entries in script-src and connect-src. Forget one and your analytics goes dark without any error on the page.

Forgetting fonts. Google Fonts needs https://fonts.googleapis.com in style-src and https://fonts.gstatic.com in font-src. The stylesheet and the actual font files come from different domains.

Not including wss:// for WebSockets. If you use real-time features, https:// and wss:// entries are separate. A WebSocket connection to wss://api.example.com won't be covered by https://api.example.com in connect-src.

Using report-uri instead of report-to. The report-uri directive is deprecated in favor of report-to, though browser support is patchy enough that using both is a reasonable hedge right now.

Setting CSP only on the main document. If you have multiple origins serving pages, you need CSP on all of them, not just the main one. An iframe loaded from a subdomain doesn't inherit the parent's CSP.

Not testing after dependency updates. Adding a new npm package that includes an inline script can silently break your CSP. CSP violations in dev are easy to miss if you're not watching the console.

Building Your First CSP

Start with the most restrictive policy you can tolerate and expand from there. A reasonable starting point for a typical content site:

Content-Security-Policy-Report-Only:
  default-src 'none';
  script-src 'self';
  style-src 'self';
  img-src 'self';
  font-src 'self';
  connect-src 'self';
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';

That will generate violations for almost everything on a real site, which is exactly what you want β€” you're mapping the terrain. Over the next few days, add the domains that show up in your violation reports. Once the violations stop, switch to the enforcing header.

The CSP Generator makes this process much faster by giving you a visual interface to build and test your policy. You can toggle directives, add domains, and get the formatted header value ready to paste. It also explains what each directive does as you build, which helps if you're new to CSP syntax.

CSP is genuinely one of those security controls where "imperfect but deployed" is much better than "perfect but never shipped." A CSP with 'unsafe-inline' is still better than no CSP. A report-only policy that you haven't tuned yet is still better than nothing. Start somewhere, improve iteratively.

Frequently Asked Questions

Share this article

XLinkedIn

Related Posts