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-inlinefor scripts - no
unsafe-inlinefor 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.