Fix CSP-Blocked Inline Styles with style-src
This guide is part of the Content-Security-Policy (CSP) reference and covers the exact directives that govern CSS: style-src, style-src-elem, and style-src-attr. When a policy without 'unsafe-inline' reaches the browser, every <style> block, every style="…" attribute, and every stylesheet inserted at runtime by a framework is checked against style-src. A block that does not match is dropped, the element renders unstyled, and the console logs a Refused to apply inline style violation. This page shows the precise values that unblock each case, how to authorize CSS by nonce or hash instead of the wildcard 'unsafe-inline', and how to keep CSS-in-JS runtimes working under a strict policy.
Configuration Syntax & Exact Values
CSS enforcement is split across three directives. style-src is the parent; style-src-elem governs stylesheet elements (<style> blocks and <link rel="stylesheet">), and style-src-attr governs inline style attributes. When the two granular directives are absent, the browser falls back to style-src for both cases.
Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-r4nd0m'; style-src-attr 'none'; img-src 'self'; object-src 'none'; base-uri 'self'
Annotated breakdown of the CSS-relevant tokens:
style-src 'self'— external stylesheets loaded from the page’s own origin are allowed. This covers<link rel="stylesheet" href="/app.css">but authorizes no inline CSS on its own.'nonce-r4nd0m'— a<style nonce="r4nd0m">block, or a<link nonce="r4nd0m">, is allowed. The nonce authorizes stylesheet elements only; it never applies tostyleattributes.style-src-attr 'none'— every inlinestyle="…"attribute is blocked. Nonces and hashes cannot rescue astyleattribute; only'unsafe-inline'or'unsafe-hashes'(with a hash) permits them.'unsafe-inline'— the blanket allow for both<style>blocks andstyleattributes. It defeats the purpose of CSP for CSS and is the token you are removing.
The critical asymmetry: a <style> element can be authorized with a nonce or a hash, but a style attribute cannot be authorized with a plain nonce or hash — it requires either 'unsafe-inline' or the 'unsafe-hashes' keyword paired with the attribute’s hash. This is why style-src-attr exists as a separate lever.
Nonce for <style> elements
A nonce is a per-response random token echoed into both the header and the matching element. Because the same value must appear in the served HTML, mint it per request — see how to generate per-request CSP nonces.
Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-Xy7Kp2Qm'
<style nonce="Xy7Kp2Qm">
.banner { background: #0b5; color: #fff; }
</style>
Hash for a static <style> element
When there is no per-request render step, hash the exact bytes between the <style> tags — identical to the hash-based approach for inline scripts, but the digest lands in style-src.
# Digest of exactly the CSS text, no surrounding tags, no trailing newline
printf "%s" ".banner{background:#0b5;color:#fff}" | openssl dgst -sha256 -binary | openssl base64
# -> uses the resulting value below
Content-Security-Policy: default-src 'self'; style-src 'self' 'sha256-Ck9mVj0lYt0V3q1r0Z0e2r4kQ0Zx8b5tPqK1uY3hE9A='
Server-Side Configuration
Every block below emits the same policy at the edge or in the application. The always / equivalent flag is mandatory: without it the header is attached only to 2xx/3xx responses, so a 304 Not Modified or a 500 error page ships without CSP and browsers render those responses under no policy at all.
Nginx
add_header inside a location or server block replaces inherited headers if any child block adds its own, so keep the CSP declaration at one level. always forces the header onto every status code.
# always -> attach on 304/4xx/5xx too, not just 2xx/3xx
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'nonce-$request_id'; style-src-attr 'none'; object-src 'none'; base-uri 'self'" always;
For deeper Nginx patterns see the Nginx security headers guide. Note that $request_id is a convenient per-request token but must match the nonce injected into the HTML by your application or SSI layer.
Apache
Header always set writes the header on all responses; set alone skips internal error documents. Place it in the VirtualHost or an .htaccess file.
# always -> covers ErrorDocument and 304 responses
Header always set Content-Security-Policy "default-src 'self'; style-src 'self' 'sha256-Ck9mVj0lYt0V3q1r0Z0e2r4kQ0Zx8b5tPqK1uY3hE9A='; style-src-attr 'none'; object-src 'none'; base-uri 'self'"
The Apache hardening guide covers mod_headers ordering and .htaccess inheritance.
Node / Express (Helmet)
Helmet’s contentSecurityPolicy middleware sets the header on every response it processes. Pass a function to inject a per-request nonce generated upstream.
const helmet = require("helmet");
const crypto = require("crypto");
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString("base64");
next();
});
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
// Helmet emits the directive on all responses it handles
styleSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
styleSrcAttr: ["'none'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
},
})
);
Render the nonce into any inline <style nonce="…">. The full setup is in the Helmet configuration guide.
Django / FastAPI
A middleware attaches the header to every response object, including error responses, so no always equivalent is needed — the object always carries it.
class CSPMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# Set on the response object -> present on all status codes
response["Content-Security-Policy"] = (
"default-src 'self'; "
"style-src 'self' 'nonce-%s'; "
"style-src-attr 'none'; object-src 'none'; base-uri 'self'"
) % request.csp_nonce
return response
See the Django and FastAPI middleware guide for wiring the nonce into templates.
Next.js / Vercel
Static header rules in next.config.js cannot mint a per-request nonce, so use hashes there or set a nonce in middleware. headers() applies the header to matched routes on every response.
// next.config.js -- static, hash-based (no per-request value possible here)
module.exports = {
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "Content-Security-Policy",
value:
"default-src 'self'; style-src 'self' 'sha256-Ck9mVj0lYt0V3q1r0Z0e2r4kQ0Zx8b5tPqK1uY3hE9A='; style-src-attr 'none'; object-src 'none'; base-uri 'self'",
},
],
},
];
},
};
The Next.js header management guide shows the middleware nonce pattern. On Cloudflare, a Transform Rule or Worker can inject the same header at the edge.
Diagnostic & Verification Steps
Confirm the header is present and correct before touching markup. curl -I prints response headers only:
curl -sI https://example.com/ | grep -i content-security-policy
Expected output — the CSP line with your style directives intact:
content-security-policy: default-src 'self'; style-src 'self' 'nonce-Xy7Kp2Qm'; style-src-attr 'none'; object-src 'none'; base-uri 'self'
To read the header on a non-2xx response and prove the always flag works, request a path that 404s:
curl -sI https://example.com/does-not-exist | grep -iE 'HTTP/|content-security-policy'
# HTTP/2 404
# content-security-policy: default-src 'self'; style-src 'self' ...
If the second line is missing on the 404, the always flag (or its framework equivalent) is not applied.
In DevTools, an unauthorized style logs a precise message in the Console:
Refused to apply inline style because it violates the following
Content Security Policy directive: "style-src 'self'".
Either the 'unsafe-inline' keyword, a hash ('sha256-…'), or a nonce
('nonce-…') is required to enable inline execution.
When the message names a style attribute, DevTools reports the violated directive as style-src-attr (or style-src on fallback). Run the policy in Report-Only first and read the document.styleSheets count against the visual result: if a block is silently dropped, its rules never appear in document.styleSheets.
// Count applied stylesheets after load -- a dropped <style> is absent here
console.log(document.styleSheets.length);
Edge Cases, Security Implications & Safe Rollback
Trap 1 — CSS-in-JS runtime injection. Libraries such as styled-components, Emotion, and MUI insert a <style> element into <head> at runtime and write rules into it via the CSSOM. Under a nonce policy, these libraries must be told the nonce so the injected element carries it. styled-components and Emotion read a __webpack_nonce__ global; MUI accepts a nonce on its cache/StyleSheetManager.
// Set before the CSS-in-JS runtime creates its <style> element
// styled-components / Emotion read this global to nonce injected styles
__webpack_nonce__ = "Xy7Kp2Qm"; // must equal the CSP nonce for this response
If the library instead mutates an existing sheet through insertRule, that path is not re-checked by CSP — the sheet was authorized once at insertion. The failure mode is the initial <style> element being blocked, not individual rules.
Trap 2 — style attributes you cannot remove. Third-party widgets, sanitized rich text, and some UI libraries write style="…" attributes directly onto elements. A nonce will not help. The clean fix is to refactor those into classed rules under a nonced or 'self' stylesheet. If refactoring is impossible, 'unsafe-hashes' 'sha256-…' (the hash of the attribute’s declaration string) authorizes specific attribute values without opening the blanket 'unsafe-inline' — but every distinct attribute value needs its own hash, and 'unsafe-hashes' widens the attack surface, so scope it to style-src-attr only.
Trap 3 — nonce leaks via CSS injection. 'unsafe-inline' on styles is not harmless: attacker-injected CSS can exfiltrate data through attribute selectors and background-image requests, and can overlay clickjacking surfaces. Removing 'unsafe-inline' from CSS is a real security gain, not cosmetic. Note that when both 'unsafe-inline' and a nonce/hash appear in the same directive, browsers that support nonces ignore 'unsafe-inline' — a useful backward-compatibility bridge for old browsers.
Rollback. Ship the policy as Content-Security-Policy-Report-Only first so violations are reported but nothing is blocked; styles keep rendering while you collect reports. To revert instantly in production, keep the previous working header value ready and swap it back at the edge — an Nginx/Apache/Cloudflare header change deploys without touching application code.
Content-Security-Policy-Report-Only: default-src 'self'; style-src 'self' 'nonce-Xy7Kp2Qm'; style-src-attr 'none'; report-uri /csp-report
Conclusion
Land this change in stages: enforce the new style-src in staging until the console is clean, then ship it to production as Content-Security-Policy-Report-Only (optionally alongside a short-lived enforcing policy on a canary) to confirm real-traffic reports show no legitimate style being dropped, and only then promote it to the enforcing Content-Security-Policy header. Keeping the prior header value one edit away means any regression is a single-line rollback.
Frequently Asked Questions
Why does my style="…" attribute still get blocked after I added a nonce to style-src?
Nonces authorize stylesheet elements, never style attributes. Attribute styles are governed by style-src-attr, which accepts only 'unsafe-inline' or 'unsafe-hashes' with a matching hash. Move the declarations into a nonced <style> block or a classed rule to fix it properly.
What is the difference between style-src-elem and style-src-attr?
style-src-elem controls <style> blocks and <link rel="stylesheet"> elements; style-src-attr controls inline style="…" attributes. When either granular directive is absent, the browser falls back to style-src for that case. Splitting them lets you allow nonced stylesheets while banning all attribute styles with style-src-attr 'none'.
Can I use a hash for an inline style attribute?
Only with the 'unsafe-hashes' keyword. Add 'unsafe-hashes' plus the 'sha256-…' digest of the attribute’s exact declaration string to style-src-attr. Each distinct attribute value needs its own hash, and 'unsafe-hashes' broadens exposure, so prefer refactoring attributes into stylesheet rules.
My styled-components / Emotion styles break under CSP — how do I fix them?
These libraries inject a <style> element at runtime, which a nonce policy blocks unless the element carries the nonce. Set __webpack_nonce__ to the current request’s nonce before the runtime creates its sheet, or pass the nonce through the library’s cache/StyleSheetManager. The value must equal the nonce in that response’s CSP header.
Does adding 'unsafe-inline' alongside a nonce weaken the policy?
No. Browsers that understand nonces ignore 'unsafe-inline' when a nonce or hash is present in the same directive, so modern browsers enforce the nonce while legacy browsers fall back to 'unsafe-inline'. It is a safe compatibility bridge, not a downgrade for supporting browsers.