Skip to main content

Technical insight · Content Security Policy, Web security, React

A strict Content Security Policy for a React website, without inline styles

How the PrajnaBytes website runs a Content Security Policy with no 'unsafe-inline' for scripts or styles, while still using React, UI libraries and dynamic effects: stylesheet classes, one per-request nonce, CSS custom properties set through the CSSOM, and self-hosted assets.

By PrajnaBytesPublished 4 min read

The problem

A Content Security Policy (CSP) tells the browser which sources of scripts, styles, images, fonts and frames a page may use. A strict policy is one of the most effective defences against injected content: if an attacker manages to place markup on a page, the browser still refuses to run or apply anything the policy does not allow.

The difficulty is that modern front-end code tends to rely on exactly what a strict policy forbids. React components set style={{ … }} attributes, animation libraries write inline styles, UI kits insert <style> elements, and third-party widgets inject both scripts and styles. Many sites end up adding 'unsafe-inline' to style-src (and sometimes script-src), which removes much of the protection.

The PrajnaBytes website is a server-rendered React application (Laravel, Inertia and React). We wanted a policy with no 'unsafe-inline' anywhere.

The approach

Every response from the website carries this policy (the Cloudflare address is added only while the Turnstile form check is switched on):

default-src 'self';
script-src 'self' [https://challenges.cloudflare.com];
style-src 'self' 'nonce-<random per request>';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'self' [https://challenges.cloudflare.com];
form-action 'self';
frame-ancestors 'self';
base-uri 'self';
object-src 'none'

Four rules make it work:

  1. Styles come from stylesheets. Components use class names from the compiled stylesheet, never style attributes. An automated test fails if a public page component contains style={.
  2. One nonce for library styles. A few UI primitives (for example the dialog's scroll lock) insert their own <style> element. A random nonce, generated for each request, is placed on those elements and in style-src, so they are allowed and nothing else inline is.
  3. Dynamic values through the CSSOM. Where a value is only known in the browser, such as the pointer position for a hover effect, the code sets a CSS custom property with element.style.setProperty('--x', value) and the stylesheet uses var(--x). MDN's documentation of style-src notes that setting the style attribute or cssText is blocked under such a policy, while properties set directly on an element's style object are not.
  4. No external origins. Fonts are self-hosted, scripts are bundled, and images come from the site itself (or data: URIs for small decorations), so 'self' is enough almost everywhere.

How it works in practice

Some patterns we used instead of inline styles:

  • Bar charts in the administration console are native <meter> elements styled in CSS, not <div>s with a computed width.
  • The page-load progress bar library is configured not to inject its own CSS; its styles are in the main stylesheet.
  • Illustrations are CSS background images on classes generated into the stylesheet by a command, not style="background-image:…".
  • Generated content for non-HTML documents, such as the human view of the XML sitemap, uses a static .css file, because the policy would block a <style> element there too.
  • Structured data for search engines is written as <script type="application/ld+json">. It is a data block that the browser does not execute, so it needs no exception in script-src.

Our automated browser audit loads every public page and fails on console errors, which include CSP violations, so a regression shows up before release.

Trade-offs

  • Discipline in every component. A single style={…} in a new component breaks the page's appearance under the policy. The automated check catches this for page components; shared components rely on review.
  • Per-request nonces. A nonce must change on every response, so HTML pages that carry it should not be served as identical copies from a shared cache.
  • Third-party widgets. Each external widget needs its own entries in the policy (here, Cloudflare Turnstile in script-src and frame-src). Some widgets require 'unsafe-inline' and cannot be used without weakening the policy; we choose widgets with that in mind.
  • A policy is not a complete defence. CSP limits the damage of injected markup. It does not replace output escaping, input validation or keeping dependencies up to date, and a script from an allowed origin can still change the page.

We build web applications with a strict policy from the start (Software and Business Systems), and can review and harden the security headers of an existing site (Technology Operations).

  • Software and Business Systems

    Custom web and mobile applications and business systems that replace manual processes, disconnected spreadsheets and ageing software.

  • Technology Operations

    Hosting, domains, certificates, maintenance, monitoring, backup and security support, so that business systems stay available and maintained.

Ask for a review of your systems

Would a strict policy break your website today? We can review its headers and the scripts it loads.

Reading is free and needs no details. We only ask for contact details if you decide to write to us.

About this publication

How it was produced. Drafted with AI assistance from our engineering records (the security-header middleware, front-end code and tests), then edited.

Review. Checked on 5 November 2026 against the policy the live website sends, its source code and automated tests, and the MDN documentation cited below.

References

  1. Content Security Policy (CSP). MDN Web Docs. Accessed 5 November 2026.
  2. Content-Security-Policy: style-src directive. MDN Web Docs. Accessed 5 November 2026.
  3. Content Security Policy Cheat Sheet. OWASP Cheat Sheet Series. Accessed 5 November 2026.

Chosen by the editor

Case studyIndependent research17 min read

AIIMS Delhi Website Ad Injection: A Historical Third-Party JavaScript Case Study

In December 2022 a visitor-counter script on the official AIIMS Delhi website was found to be placing third-party advertising at the top of the page. This case study reconstructs what happened from the original report and from archived copies of the script, separates verified facts from interpretation, and sets out what the episode teaches about third-party JavaScript.

  • Third-party JavaScript
  • Web security
  • Supply-chain risk
  • Content Security Policy

About the same service

Technical insightInternal implementation4 min read

A readable XML sitemap without XSLT, before Chrome removes it

Chrome removes XSLT from stable releases in version 158 (17 November 2026), and with it the usual way of making an XML sitemap readable in a browser. CSS on XML is not being removed. This is how our sitemap uses a plain CSS stylesheet instead, and what that approach cannot do.

  • XML sitemaps
  • Search engine optimisation
  • Web standards