Cariosan

Security headers (CSP)

Run the Cariosan SDKs under a strict Content-Security-Policy — what Cariosan needs, a reference policy, nonce setup, and a safe rollout.

6 min readUpdated Oct 11, 2026

A strict Content-Security-Policy (CSP) is your best defence against cross-site scripting. Chat content is written by your users, so a single escaping mistake anywhere in your app should not be enough to run an attacker's script. @cariosan/client and @cariosan/react are built to run under a strict policy:

  • No eval or new Function, so you never need 'unsafe-eval'.
  • No HTML string sinks: no innerHTML, no dangerouslySetInnerHTML. Messages are rendered as text.
  • No inline styles in server-rendered markup. The kit's styles come from its CSS file; the few it sets at runtime go through the CSSOM, which style-src 'self' allows.
  • No fonts, data: images or CSS url() of its own.

Every release is checked for eval-like code. The kit is tested in a real browser under the reference policy below: connecting, sending and receiving, reactions, typing, attachment upload and download, and long-history virtualisation.

What Cariosan needs from your policy

Add these origins to the directives you already have. Throughout this page, chat.example.com is your Cariosan server and files.example.com is your attachment storage (MinIO / S3; the origin of CARIOS_S3_PUBLIC_URL).

DirectiveAddWhy
connect-srchttps://chat.example.com wss://chat.example.comREST and the WebSocket. List the wss:// origin explicitly: 'self' does not cover another host.
connect-srchttps://files.example.comAttachment uploads are a PUT straight from the browser to the presigned storage URL. Without it, sending files fails.
img-srchttps://files.example.com blob:Images shown from signed download URLs; blob: for files you fetch and display yourself.
media-srchttps://files.example.com blob:Audio and video attachments.
script-srcnothingWorks with a nonce and 'strict-dynamic'. Never add 'unsafe-eval' for Cariosan.
style-srcnothingServe the kit's styles.css from your origin. If your bundler inlines it into a <style> tag, give that tag your nonce.

Reference policy

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{random per response}' 'strict-dynamic';
  style-src 'self';
  connect-src 'self' https://chat.example.com wss://chat.example.com https://files.example.com;
  img-src 'self' blob: https://files.example.com;
  media-src 'self' blob: https://files.example.com;
  font-src 'self';
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'none';
  form-action 'self';
  report-to csp
  • script-src: only scripts carrying this response's nonce run, plus the scripts those load ('strict-dynamic'). Inline event handlers (onclick="…"), eval and injected <script> markup are blocked. Generate a fresh random nonce for every response and put it on your own <script> tags.
  • 'strict-dynamic' trusts your scripts' own additions. A script you trust can still add other scripts, so keep third-party scripts to the minimum. Inline handlers, eval and inline style attributes stay blocked regardless.
  • style-src 'self' blocks style="…" attributes in HTML. If your framework or another library emits them, you will see style-src-attr reports. Moving those styles to classes is better than adding 'unsafe-inline', which for styles is a much smaller risk than for scripts.
  • frame-ancestors 'none' stops other sites from framing your chat (clickjacking). If you embed the chat in your own pages, use frame-ancestors 'self' instead.
  • Keep development and production policies apart. Development servers (Next.js, Vite) often need 'unsafe-eval' for hot reload. Allow it only in development.

Next.js (App Router)

Next.js applies the nonce to its own scripts when middleware sets the policy on the request. A nonce makes every page render dynamically, since each response needs a fresh one.

middleware.ts
import { NextResponse, type NextRequest } from "next/server";
 
const CHAT = "https://chat.example.com";
const FILES = "https://files.example.com";
 
export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const dev = process.env.NODE_ENV !== "production";
  const policy = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${dev ? " 'unsafe-eval'" : ""}`,
    "style-src 'self'",
    `connect-src 'self' ${CHAT} ${CHAT.replace("https://", "wss://")} ${FILES}`,
    `img-src 'self' blob: ${FILES}`,
    `media-src 'self' blob: ${FILES}`,
    "font-src 'self'",
    "object-src 'none'",
    "base-uri 'none'",
    "frame-ancestors 'none'",
    "form-action 'self'",
  ].join("; ");
 
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-nonce", nonce); // read it in a layout for your own <script nonce>
  requestHeaders.set("Content-Security-Policy", policy);
  const response = NextResponse.next({ request: { headers: requestHeaders } });
  // Start with Report-Only; switch the header name to Content-Security-Policy once it is quiet.
  response.headers.set("Content-Security-Policy-Report-Only", policy);
  return response;
}
 
export const config = {
  matcher: [{ source: "/((?!_next/static|_next/image|favicon.ico).*)" }],
};

Rolling it out

  1. Send the policy as Content-Security-Policy-Report-Only. Nothing is blocked; the browser only reports. Collect reports with a Reporting-Endpoints header (csp="https://your-app.example.com/csp-reports") or watch the browser console.
  2. Use the app as your users do: sign in, open conversations, send files, react, leave a tab in the background and come back. Fix each report at its source. A report naming chat.example.com or files.example.com means an origin is missing from the table above.
  3. Switch the header name to Content-Security-Policy. Keep reporting on, so a regression shows up in your reports rather than as a broken page.

Other headers worth sending

X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
  • Referrer-Policy: the SDK never puts tokens in URLs (the WebSocket carries its token in a header). Signed attachment URLs do carry a signature, and this policy keeps full URLs from leaking to other sites through the Referer header.
  • Permissions-Policy: drop a permission from the list if your app uses it, for example camera=(self) if users take photos to attach.
  • Strict-Transport-Security: serve your app and Cariosan over HTTPS only. Browsers refuse ws:// from an https:// page anyway.

Was this page helpful?

On this page