If you use Stitches, CSP gets awkward fast.
The problem is simple: Stitches is a CSS-in-JS library, and CSP is usually hostile to anything that injects styles at runtime. That does not mean they cannot work together. It means you need to be deliberate about how styles are rendered, when they are injected, and which CSP directives you allow.
I’ve seen teams ship a pretty solid script-src policy, then quietly leave style-src 'unsafe-inline' forever because CSS-in-JS made cleanup feel painful. That is usually where the security story falls apart.
Why Stitches and CSP clash
Stitches generates CSS and inserts it into <style> tags. Under CSP, inline styles are blocked unless you explicitly allow them.
That usually means one of these:
style-src 'unsafe-inline'style-src 'nonce-...'style-src 'sha256-...'
For Stitches, hashes are usually a bad fit because the CSS changes dynamically. Nonces are the practical option if you want a strict policy.
If you do nothing, you often end up with this kind of compromise:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-random123' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
That works, but style-src 'unsafe-inline' weakens the policy. If an attacker can inject HTML with a <style> block, they may be able to abuse CSS for data exfiltration tricks or UI manipulation. It is not as catastrophic as unsafe scripts, but it is still a hole.
What a real-world CSP looks like
Here’s a real CSP header from headertest.com:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-NjAxMmQ0MjAtYmRmZi00MzFkLTliMWQtMWVmYmQ1NmZjNTU2' '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'
A few things stand out:
script-srcis strong: nonce plusstrict-dynamicobject-src 'none',base-uri 'self',frame-ancestors 'none'are all goodstyle-src 'unsafe-inline'is the weak point
That weak point is common in apps using CSS-in-JS, including Stitches.
The goal
For a Stitches app, I aim for this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic';
style-src 'self' 'nonce-{RANDOM}';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
The key change is replacing:
style-src 'unsafe-inline'
with:
style-src 'nonce-{RANDOM}'
That only works if the <style> tags generated by Stitches receive the same nonce.
Basic Stitches setup
A normal Stitches config looks like this:
// stitches.config.ts
import { createStitches } from '@stitches/react';
export const { styled, css, getCssText, globalCss, theme } = createStitches({
theme: {
colors: {
primary: 'rebeccapurple',
},
},
});
And then:
// Button.tsx
import { styled } from './stitches.config';
export const Button = styled('button', {
backgroundColor: '$primary',
color: 'white',
border: 0,
padding: '10px 16px',
borderRadius: '8px',
});
This is fine for functionality. CSP is the separate problem.
Best option: render styles server-side with a nonce
If you can server-render the CSS and attach a nonce to the emitted <style> tag, you avoid depending on broad inline style allowances.
With Stitches, getCssText() gives you the generated CSS. You can inject that into the page yourself.
Here’s the pattern in a custom server-rendered React app.
Example: Express + React + Stitches + CSP nonce
Generate a nonce per request:
// server.ts
import express from 'express';
import crypto from 'node:crypto';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { getCssText } from './stitches.config';
import { App } from './App';
const app = express();
function createNonce() {
return crypto.randomBytes(16).toString('base64');
}
app.get('*', (req, res) => {
const nonce = createNonce();
const html = renderToString(<App />);
res.setHeader(
'Content-Security-Policy',
[
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data: https:`,
`font-src 'self'`,
`connect-src 'self'`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`frame-ancestors 'none'`,
].join('; ')
);
res.send(`
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style nonce="${nonce}" id="stitches">
${getCssText()}
</style>
</head>
<body>
<div id="root">${html}</div>
<script nonce="${nonce}" src="/client.js"></script>
</body>
</html>
`);
});
app.listen(3000);
That is the cleanest setup:
- new nonce per request
- same nonce in the CSP header
- same nonce on the Stitches
<style> - same nonce on inline scripts if you have them
If your app can run this way, do it.
Next.js pattern
A lot of Stitches apps live in Next.js, so this is where people get tripped up.
In a custom _document.tsx, you can collect Stitches CSS and emit it with a nonce.
// pages/_document.tsx
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from 'next/document';
import { getCssText } from '../stitches.config';
type Props = {
nonce?: string;
};
export default class MyDocument extends Document<Props> {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
const nonce = ctx.req?.headers['x-csp-nonce'] as string | undefined;
return {
...initialProps,
nonce,
};
}
render() {
const nonce = this.props.nonce;
return (
<Html>
<Head>
<style
id="stitches"
nonce={nonce}
dangerouslySetInnerHTML={{ __html: getCssText() }}
/>
</Head>
<body>
<Main />
<NextScript nonce={nonce} />
</body>
</Html>
);
}
}
Then generate the nonce in middleware or your server layer and pass it along in a request header:
// pseudo-middleware example
import crypto from 'node:crypto';
export function middleware(req, res, next) {
const nonce = crypto.randomBytes(16).toString('base64');
res.setHeader(
'Content-Security-Policy',
[
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
`object-src 'none'`,
`base-uri 'self'`,
`frame-ancestors 'none'`,
].join('; ')
);
req.headers['x-csp-nonce'] = nonce;
next();
}
The exact wiring depends on your hosting model, but the idea stays the same.
What if Stitches injects styles after hydration?
This is the annoying part.
If the library creates new <style> tags client-side after initial render, those tags also need to satisfy CSP. Some CSS-in-JS tooling lets you pass a nonce to the runtime injector. If your setup does not, you may still be forced into style-src 'unsafe-inline'.
That is why SSR matters here. The more CSS you can render up front, the less runtime style injection you need.
My rule:
- If your Stitches usage is mostly static component styling, SSR usually works well.
- If you heavily generate styles dynamically on the client, your CSP gets harder to keep strict.
Before giving up and adding unsafe-inline, test whether your app actually creates new style tags during navigation or interaction.
A practical fallback policy
Sometimes you cannot get rid of inline styles immediately. Fine. Ship a policy that is honest about that, and keep the rest strict.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
This is still much better than a sloppy policy with wildcard scripts everywhere.
If you need help understanding each directive, https://csp-guide.com is a solid reference, and the official CSP documentation on MDN is still the source I check most often.
Reporting violations while you tune the policy
When you are tightening CSP around Stitches, use report-only mode first.
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic';
style-src 'self' 'nonce-{RANDOM}';
report-uri /csp-report;
Minimal Express endpoint:
app.post(
'/csp-report',
express.json({ type: ['application/csp-report', 'application/json'] }),
(req, res) => {
console.log('CSP violation:', JSON.stringify(req.body, null, 2));
res.sendStatus(204);
}
);
If Stitches or your app runtime is still creating blocked inline styles, you will see it quickly.
Common mistakes
Reusing the same nonce for every request
Don’t. A nonce must be unpredictable and fresh per response.
Bad:
const nonce = 'static-nonce';
Good:
const nonce = crypto.randomBytes(16).toString('base64');
Using both nonce and unsafe-inline in style-src
If you leave unsafe-inline there, the nonce loses most of its value for styles.
Bad:
style-src 'self' 'unsafe-inline' 'nonce-abc123'
Pick the strict option if your app supports it:
style-src 'self' 'nonce-abc123'
Forgetting third-party sources
If your page uses analytics, consent managers, or remote fonts, your policy must reflect that.
The headertest.com example shows what happens in a real app: Google Tag Manager, Google Analytics, Cookiebot, API endpoints, WebSocket endpoints. CSP is rarely just 'self'.
Ignoring connect-src
A lot of people lock down scripts and styles, then their app breaks because fetch, sendBeacon, or WebSocket calls are blocked.
For example:
connect-src 'self' https://api.example.com wss://realtime.example.com
My recommendation for Stitches projects
If I were setting this up today, I’d do this:
- Server-render Stitches CSS with
getCssText() - Generate a fresh nonce per request
- Use that nonce in both
script-srcandstyle-src - Verify whether client-side navigation injects new blocked
<style>tags - Only fall back to
style-src 'unsafe-inline'if runtime behavior truly requires it
That gets you as close as possible to a strict CSP without pretending CSS-in-JS is free from tradeoffs.
Stitches can work with CSP. You just have to treat styles as part of your security model, not as an afterthought bolted on after the app ships.