I’ve seen the same pattern over and over: a team adopts a utility-first CSS framework, ships fast, then bolts on CSP later and wonders why the site breaks in weird places.

Pharaoh CSS is no exception.

The good news is that CSS-heavy sites are usually easier to lock down than JavaScript-heavy apps. The bad news is that most teams still start with a lazy policy like style-src 'unsafe-inline', leave it there forever, and call it “good enough”. It usually isn’t.

Here’s a real-world style case study for a developer audience, using a production CSP header as a reference point. The source header comes from headertest.com:

content-security-policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-ZDU0ZDZlYTMtY2M2MS00MmI2LTgyODYtMjhiZDliOTk4YTBm' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
  frame-src 'self' https://consentcdn.cookiebot.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none'

That’s not a bad policy. It’s actually better than what most sites deploy. It has object-src 'none', frame-ancestors 'none', base-uri 'self', and a nonce-based script-src with strict-dynamic. Those are strong choices.

But for a Pharaoh CSS site, one thing jumps out immediately:

style-src 'self' 'unsafe-inline'

That’s the part I’d want to tighten.

The setup

Picture a marketing site built with Pharaoh CSS and a small amount of JavaScript:

  • Pharaoh CSS compiled into /assets/app.css
  • A few inline theme variables in the page head
  • A consent manager
  • Google Tag Manager and analytics
  • Some SVG/data URI assets
  • No giant SPA runtime

The team started with this loose CSP because they wanted to get to production fast.

Before: the “make it work” CSP

Here’s a realistic first draft.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.google-analytics.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' data:;
  connect-src 'self' https://*.google-analytics.com https://*.googletagmanager.com;
  frame-src 'self';
  object-src 'none';
  base-uri 'self';

This usually passes basic smoke tests, but it has predictable problems.

What’s wrong here

1. unsafe-inline in style-src stays forever.
This is the classic Pharaoh CSS trap. Someone adds a tiny inline block for theme tokens or critical CSS, then the policy never evolves.

2. unsafe-inline in script-src is much worse.
If any reflected or stored XSS lands in the page, inline script execution is already allowed.

3. default-src 'self' is doing too much guessing.
You want explicit directives for scripts, styles, images, frames, forms, and connections. CSP is better when it’s boring and specific.

4. Third-party behavior isn’t modeled clearly.
Consent tools and tag managers often load more resources than expected. If you don’t account for that, the team either loosens the policy too much or disables CSP when something breaks.

The Pharaoh CSS-specific pain point

Pharaoh CSS itself is not the problem. Compiled CSS files are easy under CSP:

<link rel="stylesheet" href="/assets/app.css">

That works fine with:

style-src 'self'

The trouble starts when the app also emits inline styles like this:

<style>
  :root {
    --brand: #c49b3d;
    --surface: #111827;
    --text: #f9fafb;
  }
</style>

Or this:

<div style="--card-accent: #c49b3d">
  ...
</div>

Those patterns are common in design systems and CMS-driven themes. They’re also exactly why teams fall back to unsafe-inline.

After: a tighter CSP that still works

The better approach is:

  • keep Pharaoh CSS in external stylesheets
  • move dynamic styling into nonced <style> blocks when needed
  • avoid style="" attributes if you can
  • use nonce-based script execution instead of unsafe-inline
  • explicitly allow the third-party services you actually use

A practical improved policy looks like this:

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-{{ .CSPNonce }}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  style-src 'self' 'nonce-{{ .CSPNonce }}' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.example.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
  frame-src 'self' https://consentcdn.cookiebot.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';

This follows the same general shape as the real header from headertest.com, but swaps the weak style policy for a nonce-based one.

That one change matters.

Before and after in HTML

Before: inline styles force unsafe-inline

<head>
  <link rel="stylesheet" href="/assets/app.css">
  <style>
    :root {
      --brand: #c49b3d;
      --surface: #111827;
    }
  </style>
</head>
<body>
  <div class="hero" style="--hero-image: url('/img/hero.jpg')">
    <h1 class="text-5xl font-bold">Pharaoh CSS landing page</h1>
  </div>
</body>

This setup pushes you toward:

style-src 'self' 'unsafe-inline'

After: nonce the style block and remove style attributes

<head>
  <link rel="stylesheet" href="/assets/app.css">
  <style nonce="{{ .CSPNonce }}">
    :root {
      --brand: #c49b3d;
      --surface: #111827;
    }

    .hero {
      --hero-image: url('/img/hero.jpg');
    }
  </style>
</head>
<body>
  <div class="hero">
    <h1 class="text-5xl font-bold">Pharaoh CSS landing page</h1>
  </div>
</body>

Now you can use:

style-src 'self' 'nonce-{{ .CSPNonce }}'

That’s a much cleaner tradeoff.

Server-side nonce generation

If you’re going to use nonce-based CSP, generate a fresh nonce per response and apply it consistently to both the header and allowed inline blocks.

Here’s a simple Node/Express example:

import crypto from "node:crypto";
import express from "express";

const app = express();

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

  res.setHeader(
    "Content-Security-Policy",
    [
      "default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com",
      `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com`,
      `style-src 'self' 'nonce-${nonce}' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com`,
      "img-src 'self' data: https:",
      "font-src 'self'",
      "connect-src 'self' https://api.example.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com",
      "frame-src 'self' https://consentcdn.cookiebot.com",
      "frame-ancestors 'none'",
      "base-uri 'self'",
      "form-action 'self'",
      "object-src 'none'"
    ].join("; ")
  );

  next();
});

app.get("/", (req, res) => {
  res.send(`
    <!doctype html>
    <html>
      <head>
        <link rel="stylesheet" href="/assets/app.css">
        <style nonce="${res.locals.cspNonce}">
          :root { --brand: #c49b3d; }
        </style>
      </head>
      <body>
        <h1 class="text-5xl font-bold">Pharaoh CSS</h1>
        <script nonce="${res.locals.cspNonce}">
          console.log("allowed by CSP nonce");
        </script>
      </body>
    </html>
  `);
});

That’s the pattern I trust in production: one nonce, per response, used only where needed.

What changed operationally

Once the team moved from the “before” policy to the “after” one, three things got better fast.

1. Inline styling became intentional

Before, anyone could toss a style="" attribute or random <style> block into a template and it would work. That sounds convenient until you’re trying to control risk across a real app.

After tightening CSP, styling had to go through one of these paths:

  • compile it into Pharaoh CSS output
  • put it in a static stylesheet
  • use a nonced style block in a server-rendered template

That friction is useful. It catches bad habits early.

2. Script security improved a lot

The real security win often comes from fixing script-src, not just style-src. The headertest.com header gets this mostly right with:

script-src 'self' 'nonce-...' 'strict-dynamic'

I like this approach because it scales better than giant host allowlists. If your trusted bootstrap script has a valid nonce, strict-dynamic lets that trust flow to scripts it loads. If you want a deeper breakdown of that behavior, the official CSP spec and the directive guides at https://csp-guide.com are worth reading.

3. Third-party scope became visible

Consent tools, analytics, tag managers, embedded frames — these are where CSP gets messy. A well-written policy forces the team to inventory them instead of pretending they don’t exist.

That’s one reason the reference header is useful. It shows a realistic compromise: strong core directives, explicit third-party allowances, and a clearly defined frame and connect surface.

A few blunt recommendations for Pharaoh CSS projects

Prefer external CSS over inline critical CSS

If you can preload and serve a compiled stylesheet, do that. Don’t use inline CSS just because it feels easy.

If you must inline styles, use a nonce

For server-rendered templates, nonce-based <style> blocks are usually the cleanest answer.

Avoid style="" attributes

They’re annoying under CSP and tend to spread. I’ve rarely regretted refactoring them out.

Don’t copy giant CSPs blindly

A policy from another site is just a starting point. Your analytics stack, embeds, font hosting, and app architecture decide the final shape.

Keep the strong directives

These are low-drama, high-value:

object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';

I add them early unless there’s a real reason not to.

The final result

For a Pharaoh CSS site, the best outcome usually looks pretty boring:

  • stylesheet served from self
  • no unsafe-inline for scripts
  • no unsafe-inline for styles if you can help it
  • nonce any unavoidable inline blocks
  • explicitly allow only the third-party origins you actually need

That’s the difference between “we have CSP enabled” and “our CSP is actually doing something useful”.

If your current policy still says:

style-src 'self' 'unsafe-inline'

I’d treat that as technical debt, not a final design.