Table of Contents

Content-Security-Policy at the Gateway

The Neos Gateway adds a Content-Security-Policy header to every response it sends. The browser then refuses to load the resources that the policy does not allow, which limits the damage of an injected resource or of an external resource in a tenant visual identity.

This article describes the default policy, how to replace or disable it, how the Gateway applies it, and a stricter policy you can start from.

Default policy

A Neos deployment made with the Helm chart enforces this policy by default:

style-src 'self' 'unsafe-inline'

The browser only loads the stylesheets served by the application itself. The other resources (scripts, images, fonts, connections) are not restricted by the default policy.

Warning

A stylesheet loaded from another origin, for example a font or a CSS framework from a CDN, or an @import of an external URL in a tenant CSS, is blocked. If your deployment loads such a stylesheet, replace the default policy with one that allows its origin, see Replace the default policy.

'unsafe-inline' is required because the client writes the CSS of the active theme into an inline <style> element. See Why style-src keeps 'unsafe-inline'.

Settings

The policy is set with two Helm values on the gateway property of the main configuration (see Helm configuration):

  • contentSecurityPolicy: the policy, on a single line. An empty or blank value means the default policy.
  • contentSecurityPolicyEnabled: true by default. false removes the policy added by the Gateway. The sandbox policy that the clusters send with the files they serve is kept, see the note in How the Gateway applies the policy.

Replace the default policy

A configured policy replaces the default one entirely: it is not merged with it. Keep style-src 'self' 'unsafe-inline' and add the origins you need:

gateway:
  contentSecurityPolicy: "style-src 'self' 'unsafe-inline' https://cdn.example.com"

Disable the policy

gateway:
  contentSecurityPolicyEnabled: false

The chart then renders no ContentSecurityPolicy section in the configuration of the Gateway, and the Gateway adds no policy. The responses keep the Content-Security-Policy header sent by the clusters, such as the sandbox policy of the files they serve.

contentSecurityPolicy must then be empty: a policy set while contentSecurityPolicyEnabled is false makes the chart rendering fail, so helm install and helm upgrade stop before deploying anything.

Configuration of the Gateway

The chart writes the policy to the configuration key Gateway:ContentSecurityPolicy:Policy of the Gateway. The Gateway reads it when it starts, so a change needs a restart of the Gateway. Without that key, or with an empty value, the Gateway adds no policy.

Local development

In local development, the application is served by the server proxy, not by the Gateway, and the Helm values do not apply. The server proxy sends the default style-src 'self' 'unsafe-inline' policy too, so a stylesheet that the default policy blocks once deployed is already blocked locally. A custom policy can only be checked on a deployed environment, see Check a custom policy.

How the Gateway applies the policy

  • Every response. The header is set on every response of the Gateway: static pages, status code pages, the tenant selection page, redirects, API responses, and the responses it proxies from the clusters and their frontends.
  • It replaces an upstream header, except a sandbox policy. When a proxied service already sends a Content-Security-Policy header, the Gateway value replaces it. The exception is an upstream value that contains a sandbox directive: the Gateway keeps it and adds its own policy as an additional value, and the browser enforces both. The Gateway never merges the text of two policies into one value.
  • Invalid policies are ignored. A policy that contains a control character (for example a line break) or a character that is not ASCII is ignored, with a warning in the Gateway log, and no header is sent. A multi-line value, for example a YAML block scalar written with |, therefore disables the feature: keep the policy on one line.
  • Surrounding spaces are trimmed.
Note

The framework serves the files of the application (files of entities and server methods, exports, reports, images of a tenant style) with the header Content-Security-Policy: sandbox, except PDF files, so that a file opened directly in the browser cannot run a script. The Gateway keeps that header and adds its own policy next to it: the sandbox protection of those files is not lost.

A stricter policy

The default policy only covers stylesheets. This policy restricts every kind of resource and is a starting point for a stricter deployment:

default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; worker-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'
Important

This policy has not been enforced in a real browser against every screen of the framework. Check it on a non-production environment first, see Check a custom policy.

Directive Why
default-src 'self' Everything the application loads comes from its own origin: the scripts, the stylesheets, the fonts and the images are served by the Gateway and the clusters. No CDN reference was found in the client sources.
style-src 'self' 'unsafe-inline' See Why style-src keeps 'unsafe-inline'. A tenant stylesheet is served from the same origin, so 'self' allows it.
img-src 'self' data: The framework images and the images of the tenant styles are served from the same origin. data: is included because components commonly embed small images as data URIs: remove it if your checks show no use of it.
font-src 'self' The fonts of the themes (Golos Text, Montserrat, Roboto) are files of the application.
worker-src 'self' The code editor (Monaco), used for example by the CSS editor of Tenant Management, starts its language workers from files bundled with the application, not from a CDN.
connect-src 'self' The API calls and the notification connection go to the same origin. See Notifications.
object-src 'none' The application does not use plugins.
base-uri 'self' Prevents an injected base element from redirecting relative URLs.
frame-ancestors 'self' Only the application itself can embed its pages in a frame. Change it if the application must be embedded by another origin.

script-src is not set, so it falls back to default-src 'self': inline scripts are blocked. None were identified in the client sources, but this was not checked against every generated page.

Why style-src keeps 'unsafe-inline'

A strict style-src 'self' alone is not possible today. The theme manager of the client writes the CSS of the active theme into an inline <style data-theme> element (see client/packages/shared/src/theme/ThemeManager.ts), and another component of the design system creates style elements as well. A browser blocks an inline <style> element unless style-src allows 'unsafe-inline', or a nonce or a hash that matches its content. The content depends on the theme and is built in the browser, so neither can be provided by the Gateway. Removing the need for 'unsafe-inline' is a change in the client, not a configuration.

The third-party components of the application, such as the code editor, can also create inline styles. This was not checked one by one.

Notifications

The client opens its notification connection with SignalR, with the default transport negotiation: WebSocket first, then server-sent events or long polling when WebSocket is unavailable. All of them go to the same host. The WebSocket uses the ws: or wss: scheme, and depending on the browser 'self' may not cover it. If the browser blocks the notification connection, add the secure WebSocket origin to connect-src, for example connect-src 'self' wss://neos.example.com.

If you enabled Application Insights in the browser, add the ingestion endpoint of its connection string to connect-src as well.

Tenant styles and external resources

A tenant stylesheet and its images are served by the cluster on the application origin, so 'self' allows them. An external origin in a tenant CSS is blocked:

  • @import url(https://fonts.example.com/...) is blocked by style-src, including with the default policy.
  • background-image: url(https://cdn.example.com/logo.png) is blocked by img-src, with a policy that sets it.
  • @font-face with an external src is blocked by font-src, with a policy that sets it.

Upload the images in the CSS and reference them with a relative url(assets/<name>) instead. See Tenant visual identity.

Check a custom policy

  1. Deploy the custom policy on a non-production environment.
  2. Use the application as the users do: sign in, select a tenant, open the main views, the reports, the code editor, the notifications and a tenant style if you use them. Keep the browser console open.
  3. The console lists each blocked resource, with the directive and the blocked URL. Add the origin to the right directive when the resource is legitimate, or remove the resource.
  4. When a full pass produces no violation, deploy the policy to production.

To collect the violations of several users, add a report-uri or report-to directive that points to a collector you operate. The framework does not provide one.

See also