Olympus Docs
CookbookDefensive security

Tighten CSP to nonce-based

From baseline to nonce-only, the strictest CSP

Olympus ships with a reasonable CSP. For higher-security deployments, tighten further: nonce-based scripts, no inline anything, Trusted Types.

Baseline

default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
frame-ancestors 'none';

unsafe-inline permits any inline script, including XSS-injected scripts. Not great.

Step 1: Remove unsafe-inline from scripts

script-src 'self' 'nonce-{random}';

Each <script> tag must have nonce="{random}" matching the value.

For Next.js:

// app/layout.tsx
import { headers } from "next/headers";

export default function Layout({ children }: { children: React.ReactNode }) {
  const nonce = headers().get("x-nonce")!;
  return (
    <html>
      <head>
        <Script nonce={nonce}>
          {`
            window.appConfig = ${JSON.stringify(config)};
          `}
        </Script>
      </head>
      <body>{children}</body>
    </html>
  );
}

Middleware generates per-request nonce:

// middleware.ts
import { NextResponse } from "next/server";

export function middleware(req) {
  const nonce = btoa(crypto.getRandomValues(new Uint8Array(16)));
  const csp = `default-src 'self'; script-src 'self' 'nonce-${nonce}'; ...`;
  const res = NextResponse.next();
  res.headers.set("content-security-policy", csp);
  res.headers.set("x-nonce", nonce);
  return res;
}

Step 2: Remove unsafe-inline from styles

Inline styles (style="...") need similar treatment:

style-src 'self' 'nonce-{random}';

But: many UI libraries use inline styles. Audit:

  • Tailwind: emits CSS classes, NOT inline. Safe.
  • Emotion / styled-components: inline by default. Often have nonce option.

For maximum compat:

style-src 'self' 'unsafe-inline';  // still risky but unavoidable for some libs

Step 3: strict-dynamic

script-src 'nonce-{random}' 'strict-dynamic';

strict-dynamic lets trusted nonce'd scripts load additional scripts dynamically. Loosens, but only for scripts the trusted ones brought in.

Reduces nonce maintenance, you don't need to nonce every dynamic load.

Step 4: Trusted Types

require-trusted-types-for 'script';

Code that does element.innerHTML = string fails unless string is a Trusted Type. Forces safe DOM API use.

// Define a policy
trustedTypes.createPolicy("default", {
  createHTML: (input) => DOMPurify.sanitize(input),
});

// Now this works safely:
element.innerHTML = "<p>" + userInput + "</p>";

Step 5: Restrict img / font / connect sources

img-src 'self' https://cdn.your-domain.com data:;
font-src 'self' https://fonts.yourcdn.com;
connect-src 'self' https://api.your-domain.com https://hydra.your-domain.com;

Don't blanket * or https:. Specific origins.

Step 6: report-to

For violations, get reports:

Content-Security-Policy: ...
Report-To: { "group": "csp-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://your-domain.com/csp-report" }] }
report-to csp-endpoint;

Handler:

app.post("/csp-report", (req, res) => {
  console.log("CSP violation:", req.body);
  // Send to monitoring
  res.sendStatus(204);
});

You'll see attempted violations (XSS attempts, misconfigured CDN, etc.).

Step 7: report-only first

Test before enforcing:

Content-Security-Policy-Report-Only: <new policy>
Content-Security-Policy: <old policy>

Both headers; old one enforced, new one only reports. Watch reports for a week. When clean, switch.

Common violations and fixes

Inline event handlers

<button onclick="doSomething()">Click</button>

Disallowed. Use event listeners:

<button id="btn">Click</button>
<script nonce="X">
  document.getElementById("btn").addEventListener("click", doSomething);
</script>

Third-party scripts

Google Analytics, Sentry, etc., need allowance:

script-src 'self' 'nonce-X' https://www.googletagmanager.com https://browser.sentry-cdn.com;

Limit to specific origins, not *.

eval() and new Function()

script-src 'self' 'nonce-X';

Blocks eval. Some libraries (older Vue templates, etc.) use it. Update or allow:

script-src 'self' 'nonce-X' 'unsafe-eval';  // weakens; avoid if possible

data: URLs

img-src 'self' data:;

data: is risky, can host SVG with JS. Better:

img-src 'self' https://cdn.your-domain.com;

If you must allow data: for images (placeholders, etc.), do so but no SVG:

img-src 'self' data:;
script-src ...;  // data: not in script-src, SVG with script blocked

Reporting infrastructure

For high traffic, CSP reports can be high-volume. Aggregate:

  • Send to a logging service (Datadog, Sentry).
  • Filter known false positives (e.g., Chrome extensions).
  • Alert on unexpected sources.

Implementation effort

Going from baseline to full nonce/Trusted Types:

  • Day 1: enable report-only with strict policy.
  • Day 2-3: review reports, fix legitimate violations.
  • Week 1: clean reports.
  • Day 8: switch to enforce.

For Olympus's Hera/Athena codebase, this took ~3 days of work in our migration.

On this page