Material UI is one of those libraries that makes frontend teams fast and security teams nervous.

The problem is simple: MUI relies on runtime-generated CSS. CSP, if you configure it properly, is hostile to runtime-generated inline styles unless you make an exception. That tension forces a choice: loosen your policy, wire up nonces correctly, or rethink how styles are injected.

If you’re building with MUI, the right CSP setup depends on which styling path you’re using:

  1. style-src 'unsafe-inline': easiest, weakest
  2. style-src 'nonce-...': best security/usability balance
  3. hash-based CSP for styles: usually a bad fit for MUI
  4. static extracted CSS: strongest in theory, often not how MUI is used

I’ve had to fix this more than once in React apps that passed local testing and then broke the second CSP was enabled in staging. MUI works fine with CSP, but only if you treat styling as part of your security design instead of an afterthought.

Why MUI conflicts with CSP

Modern MUI uses Emotion by default. Emotion injects styles into <style> tags at runtime. A strict CSP blocks those tags unless they carry a valid nonce or you allow inline styles globally.

So this policy will usually break MUI:

Content-Security-Policy: default-src 'self'; style-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';

Why? Because Emotion tries to insert style blocks, and CSP says “no inline styles unless explicitly allowed.”

If you want a refresher on how style-src, nonces, and strict policies work, the official reference is worth keeping open:

Option 1: unsafe-inline for styles

This is the common shortcut.

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'self';

Pros

  • Dead simple
  • MUI usually works immediately
  • No nonce plumbing through SSR, templates, or React rendering

Cons

  • Weakens CSP in a meaningful way
  • Inline style injection is allowed everywhere, not just for MUI
  • Makes style-based XSS impact worse
  • Security reviewers will notice, and they should

This is the same pattern visible in the real 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-NGI3MDk2NTUtMTU1OC00NzE3LThiMTEtY2Y4NWY4M2ZmNmZj' '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 a pretty realistic production compromise. Their scripts are nonce-based and fairly modern with 'strict-dynamic', but styles still fall back to 'unsafe-inline'. I see this a lot: teams get serious about script-src first, then leave style-src relaxed because CSS-in-JS frameworks are annoying under CSP.

If you need a short-term fix to stop MUI from breaking, this works. I just wouldn’t call it “done.”

Option 2: Nonce-based CSP for MUI

This is the setup I recommend for most production apps.

The idea is:

  • Generate a fresh nonce per request
  • Put it in your CSP header under style-src
  • Pass the same nonce to Emotion so its <style> tags are allowed

Example header:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0m123';
  style-src 'self' 'nonce-r4nd0m123';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';

Pros

  • Stronger than 'unsafe-inline'
  • Works well with MUI and Emotion
  • Good fit for SSR frameworks like Next.js
  • Easier to defend in a security review

Cons

  • More moving parts
  • You need per-request nonce generation
  • SSR and hydration need to agree on the nonce
  • Easy to misconfigure in dev/prod differences

Here’s a Node/Express example generating a nonce:

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.nonce = nonce;

  res.setHeader(
    'Content-Security-Policy',
    [
      "default-src 'self'",
      `script-src 'self' 'nonce-${nonce}'`,
      `style-src 'self' 'nonce-${nonce}'`,
      "img-src 'self' data:",
      "font-src 'self'",
      "connect-src 'self'",
      "object-src 'none'",
      "base-uri 'self'",
      "frame-ancestors 'none'",
    ].join('; ')
  );

  next();
});

Then wire Emotion to use the nonce:

import createCache from '@emotion/cache';

export function createEmotionCache(nonce) {
  return createCache({
    key: 'mui',
    nonce,
    prepend: true,
  });
}

In your React app:

import { CacheProvider } from '@emotion/react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { createEmotionCache } from './createEmotionCache';

const theme = createTheme();

export default function App({ nonce }) {
  const cache = createEmotionCache(nonce);

  return (
    <CacheProvider value={cache}>
      <ThemeProvider theme={theme}>
        {/* app */}
      </ThemeProvider>
    </CacheProvider>
  );
}

This is the MUI-friendly answer. If your app server already injects request-specific data into HTML, adding a nonce is not a huge leap.

Option 3: Hash-based style CSP

Technically possible. Practically annoying.

A hash-based policy looks like this:

Content-Security-Policy: style-src 'self' 'sha256-AbCdEf123...';

Pros

  • Very strict
  • No need for 'unsafe-inline'
  • Good for truly static inline blocks

Cons

  • Bad match for dynamically generated MUI styles
  • Hashes change when style output changes
  • Hard to maintain across theme changes, rendering paths, and SSR
  • Becomes miserable fast

For MUI, this is usually the wrong tool. Hashes shine when you have fixed inline snippets. Emotion-generated CSS is not fixed enough to make this pleasant.

I would not choose this unless you have a very unusual setup with deterministic, stable style output and strong build-time control.

Option 4: Static CSS extraction or avoiding runtime injection

This is less “how to configure MUI” and more “how to avoid the CSP problem entirely.”

If your styling system outputs static CSS files and your policy is:

Content-Security-Policy: default-src 'self'; style-src 'self'; script-src 'self'; object-src 'none';

then life gets easier.

Pros

  • Strong clean CSP
  • Fewer runtime exceptions
  • Simpler security posture
  • Easier for auditors to understand

Cons

  • Not the default MUI developer experience
  • May require architectural changes
  • Loses some flexibility of CSS-in-JS
  • Not always realistic in an existing app

If your team is early in a project and CSP is a hard requirement, this tradeoff is worth discussing. Retrofitting a strict CSP onto a CSS-in-JS-heavy app is always more painful than planning for it upfront.

My take on the real-world tradeoff

Here’s how I’d rank the options for most teams using MUI:

Best practical choice: nonce-based style-src

Strong enough, compatible with MUI, and not too painful once the plumbing exists.

Acceptable temporary compromise: 'unsafe-inline' for styles

If you’re trying to ship a CSP without breaking production, this is often the first stable version. Just don’t pretend it’s the end state.

Usually avoid: hashes for MUI styles

They fight the framework.

Strongest policy, biggest product tradeoff: move away from runtime style injection

Security likes it. Frontend teams may not.

A production-friendly baseline policy for MUI

If you’re using MUI with a nonce and a few common integrations, a policy might look something like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
  style-src 'self' 'nonce-{RANDOM_NONCE}';
  img-src 'self' data: https:;
  font-src 'self' https:;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';

If you need directive-by-directive background, csp-guide.com is useful for the deeper CSP semantics.

What usually breaks during rollout

These are the failures I see most often:

  • Nonce added to scripts but not Emotion styles
  • Server-generated nonce differs from client-side nonce
  • SSR output works, client hydration injects blocked styles later
  • Third-party widgets silently require extra style-src or connect-src entries
  • Teams copy a CSP from another app without checking MUI’s injection behavior

Test with the browser console open. CSP errors are usually explicit, and they’ll tell you exactly which directive blocked what.

Recommendation

If you’re using Material UI today, I’d choose one of these two paths:

  1. Short term: style-src 'self' 'unsafe-inline' so the app works
  2. Real fix: move to a nonce-based CSP and pass the nonce into Emotion

That mirrors what a lot of mature teams do in phases. Even the headertest.com example shows a strong script policy paired with a weaker style policy. That’s not unusual. It’s just a sign that MUI and CSP need deliberate integration work.

If you want a CSP that’s both secure and realistic for MUI, nonces are the sweet spot.