Diego Betto's Blog
Photo by Miguel Á. Padriñán

May 1, 2024 · 7 min di lettura

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.

Condividi:XLinkedInFacebookWhatsApp

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:

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 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
script-src 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

Other directives that are often overlooked but very interesting are:

directive content
form-action Restricts the URLs that can be used as a target for forms
frame-ancestors 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.

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.

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.
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

<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.

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.

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.

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.

<!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>
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:

{
  "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:

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.

More on this soon — happy coding!

Condividi:XLinkedInFacebookWhatsApp