# CSP: how Content Security Policy works

> Content Security Policy, or CSP, is a set of rules that tell the browser how to behave when loading the content of our HTML page.

- Autore / Author: Diego Betto (https://diegobetto.com/about/)
- Pubblicato / Published: 2024-05-01
- URL: https://diegobetto.com/en/content-security-policy-csp-guide/
- Tag: csp, content security policy, security, virus, web, html, javascript

## The content of a web page

When we browse a website, some content — **CSS stylesheets**, **images**, **JavaScript scripts** — is often loaded from separate files.

> I say "often" because, if we wanted to, we could embed CSS, JS and images entirely inside our HTML page, but that's a topic for another article.

This content can live on the same server, or it might be a resource loaded from external sources, from other sites or from a **CDN**.

Unfortunately, it's not only the content we consider necessary and legitimate that gets loaded this way.
Sometimes **viruses** and **malware**, after infecting a website, try to load payloads from external sources. They do this for various reasons:

- to use a simpler, lighter piece of malware to inject into the site's code;
- to let the malware download updated modules on the fly;
- to make identification harder.

## How can we avoid unwanted content?

There are many ways, more or less standard, to limit the risk. Using WAFs, i.e. **Web Application Firewalls**, definitely helps, reducing the chance that anomalous or dangerous behavior compromises the integrity of our website.
This can reduce the attack surface both on the front-end and on the back-end.
Personally, I get along very well with tools like:

- [Cloudflare WAF](https://www.cloudflare.com/application-services/products/waf/)
- [Wordfence](https://www.wordfence.com/)

Software and services of this kind are very convenient, easy to integrate, and well tested.

But often that's not enough. Some infections can still get through. There's no method that guarantees 100% security for your site. If someone promises you that, they're generally lying.
What we can do, though, is make attackers' lives harder.

## What is CSP?

**Content Security Policy**, or **CSP** for short, is a set of rules that tell the browser how to behave when loading the content of our HTML page.
It adds a layer of security to our site, especially against [**Cross-Site Scripting**](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting) attacks, known as **XSS**.

We have two ways to send this policy to the browser:

- via the **Content-Security-Policy** HTTP header;
- via a meta tag inside the page itself.

A CSP rule tells the browser that content can only be loaded from certain sources that the developer or administrator considers valid and safe.
Besides external sources, we can also specify rules for **inline** content, i.e. CSS, JavaScript etc. present directly in our page.

In extreme cases, you can even forcibly disable the execution of JavaScript scripts, from any source.

## What do CSP rules look like?

CSP rules are made up of a series of **directives**, each describing the policy for a given type of content. Let's go through them.

### The base rule, `default-src`

This is the default policy, and it should always be present. It indicates how the browser should behave for content that has no specific policy.

| directive                 | content                                                                                                       |
| ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| <nobr>`script-src`</nobr> | Used to specify the policy for scripts. If present, it overrides `default-src` for scripts                    |
| `style-src`               | Specifies the policy for the site's stylesheets. Like `script-src`, it overrides the default policy           |
| `font-src`                | Specifies the policy for fonts loaded via `@font-face`, overriding the default policy                         |
| `media-src`               | Specifies the policy for content loaded via `<audio>`, `<video>` and `<track>`, overriding the default policy |
| `frame-src`               | Specifies the policy for content loaded via `<frame>` and `<iframe>`, overriding the default policy           |
| `img-src`                 | Specifies the policy for images and favicons, overriding the default policy                                   |

For the full list, see [MDN — Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)

Other directives that are often overlooked but very interesting are:

| directive                      | content                                                   |
| ------------------------------ | --------------------------------------------------------- |
| <nobr>`form-action`</nobr>     | Restricts the URLs that can be used as a target for forms |
| <nobr>`frame-ancestors`</nobr> | Specifies the valid parents allowed to embed our page     |

### CSP as a header, a few examples

This rule loads content only from the same domain, excluding subdomains too.

```http
Content-Security-Policy: default-src 'self'
```

If instead we also want to load content from another domain and its subdomains, we can write something like this.

```http
Content-Security-Policy: default-src 'self' example.com *.example.com
```

We can combine multiple directives:

- a default one that only accepts content from the main domain;
- an `img-src` that allows loading images from any origin;
- a `media-src` that allows loading content only from the two specified domains;
- a `script-src` that allows loading scripts only from the single specified third-level domain.

```http
Content-Security-Policy: default-src 'self'; img-src *; media-src example.org example.net; script-src userscripts.example.com
```

### CSP as a meta tag

If instead we wanted to send the policy via a `meta` tag, we could set it up as follows

```html
<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; img-src https://*; child-src 'none';"
/>
```

> **Careful**: if you use the meta form, you can't send CSP violations as we'll see shortly. That's only possible through the header form.

## How do you test whether the policy works correctly?

When we set up **CSP rules**, it's always worth testing that everything works correctly.
Sometimes a piece of content or a behavior on our site stops working like before.
A classic case is when we set up directives for **web analytics** scripts that then try to load other utility scripts from other domains.

We can proceed manually, checking in the console which scripts no longer load and adding the missing domains, fixing the rules as we go.

But if the site is live, it might not be pleasant to find it broken.

Instead, **only in the header form**, we can send the `Content-Security-Policy-Report-Only` header, which specifies that the given rules are in test mode only — content isn't blocked, and any issues are instead reported to the specified address.

```http
Content-Security-Policy-Report-Only: default-src https:; report-to /endpoint-where-reports-are-sent/
```

We can also specify that the policy should be enforced, so content that doesn't comply gets blocked, while at the same time sending the issues to our endpoint.
To do this we use the previous header, `Content-Security-Policy`.

```http
Content-Security-Policy: default-src https:; report-to /endpoint-where-reports-are-sent/
```

The **report** sent is a **JSON object** with a `Content-Type` of **application/csp-report**, containing all the fields detailed in [the MDN "Violation report syntax" docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only#violation_report_syntax).

## Example

Taken directly from the official documentation, here's an example with an HTML page, a rule sent via header, and the report that will arrive at the endpoint specified in the directive.
Let's assume this page lives at `http://example.com/signup.html`.

```html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
    <title>Sign Up</title>
    <link rel="stylesheet" href="css/style.css" />
  </head>
  <body>
    Page content
  </body>
</html>
```

```http
Content-Security-Policy-Report-Only: default-src 'none'; style-src cdn.example.com; report-to /_/csp-reports
```

In this case, the stylesheet can only be loaded from the `cdn.example.com` domain, as specified in the rule.
Our HTML page, though, contains a `link` tag that tries to load from a relative path, and therefore from the site's own origin.
This is a violation, and since we specified that `report-to` should send issues, we'll receive a JSON object like this:

```json
{
  "csp-report": {
    "blocked-uri": "http://example.com/css/style.css",
    "disposition": "report",
    "document-uri": "http://example.com/signup.html",
    "effective-directive": "style-src-elem",
    "original-policy": "default-src 'none'; style-src cdn.example.com; report-to /_/csp-reports",
    "referrer": "",
    "status-code": 200,
    "violated-directive": "style-src-elem"
  }
}
```

## Conclusion

As we've seen, we can improve our site's security by adding an extra layer of protection, making life harder for bots and attackers.

News of all sorts of attacks, even against large organizations, is a daily occurrence.
Take a look, for example, at these excellent websites:

- [RHC Red Hot Cyber](https://www.redhotcyber.com/)
- [Ransomfeed](https://ransomfeed.it/)

As already mentioned, no system, alone or combined with others, gives you absolute certainty that a site can't be attacked. The available attack surface stays wide. The CMS, the server software, the server hardware, the people involved can all be an entry point for attackers.

Regardless of your protection systems, one piece of advice always holds: **back up often, verify your backups, and store them elsewhere, ideally offline**.

Staying on the security topic, you might also be interested in the recap of the [Node.js January 2026 security releases](https://diegobetto.com/nodejs-security-release-2026/).

More on this soon — happy coding!
