Skip to content

web: serve a per-request CSP nonce so MUI's Emotion styles apply under style-src 'self' #873

Description

@mforce

Part of #674. Groundwork, sequenced before #864's overrides and every screen slice.

The finding (from #871, 2026-09-14)

The production Content Security Policy is style-src 'self' (src/Cluckwork.Api/Security/SecurityHeaders.cs). MUI styles through Emotion, which injects a <style data-emotion> element at runtime, and the browser refuses it under that policy. Measured on the sim harness at 390: the style element is in the document, html computes box-sizing: content-box where CssBaseline sets border-box, /daily-entry lays out 419px wide in a 390px frame, and the console reports "Applying inline style violates the following Content Security Policy directive style-src 'self'". The same path carries every sx, styled() and styleOverrides value, so no MUI styling reaches the screen until this is settled. It was invisible until #871 because no MUI component rendered before it.

The decision (owner, 2026-09-14): a per-request nonce, policy stays strict

Rejected: 'unsafe-inline' on style-src (re-opens CSS injection, including attribute-selector exfiltration; a security regression the record would have to own) and build-time CSS extraction (fights the runtime palette-from-the-document design in docs/decisions/674-ui-component-library.md).

Scope

  • API. SecurityHeaders mints a nonce per response and emits style-src 'self' 'nonce-<value>' (keep 'self' so styles.css still loads). The SPA's index.html is served as a small templated response that carries the same nonce in a <meta name="csp-nonce" content="..."> (or equivalent), instead of a static file from wwwroot; hashed /assets/* stay static and immutable (Static asset Cache-Control headers + Cloudflare-in-front CDN strategy #141). The revalidation semantics index.html has today must survive (it is the update-detection path for the PWA, Installable PWA baseline — manifest, icons, service worker app-shell cache (split from #50) #142).
  • SPA. FarmThemeProvider wraps its children in Emotion's CacheProvider with a cache created with that nonce (createCache({ key, nonce })), read once from the meta tag at boot. No other code reads the nonce.
  • Tests. An integration test that the served index.html's nonce equals the header's and differs between two requests; a unit test that the cache is built with the meta nonce and that a missing meta falls back to no nonce in Development only (Production fails closed, the header would block styles anyway); a Playwright assertion on the sim harness that a rendered MUI component's computed style is applied under the real CSP (the console must carry no CSP violation). SecurityHeadersTests (or the equivalent) updated with the new directive shape.
  • Docs. docs/decisions/674-ui-component-library.md gains an amendment recording the finding and this answer; docs/architecture.md if it draws the SPA-serving path; AGENTS.md only if a rule changes (none expected). Sim harness (#243) rotted silently: 4 breakages, no CI ever ran it #370: SecurityHeaders is code, not configuration, so the sim harness needs no new key; say so in the PR. Developer experience: add an Aspire AppHost for local orchestration and observability #565: same for the AppHost.

Done when

  • A rendered MUI Button on the sim harness (Production CSP) shows its themed background with no CSP violation in the console, proved by the Playwright assertion above.
  • The nonce differs per request and the served page's meta equals the header, proved by the integration test.
  • Before/after screenshots at 1280 and 390 of one screen with an MUI component under the real CSP, captured from a stack rebuilt at the head under review.

Constraints inherited

Strict CSP is a deliberate property of this deployment (see the header comments in SecurityHeaders.cs and #141); script-src 'self' and the external pre-paint script are untouched. The nonce must come from a cryptographic RNG and never be logged. index.html remains cacheable only as it is today.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:frontendReact/Vite web clientepic-674SPA revamp — field-first phone, ledger desktop (#674)phase:1-groundworkWhole-app decisions before any screen convertspriority:tier2High value, low risksliceThin vertical work item

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions