Insights | Dataweavers

Security Headers and CSP in Headless Sitecore

Written by Piers Matthews | Apr 30, 2026 11:00:00 AM

Why "We Have HTTPS" is Not Enough

Most Sitecore teams, when asked about web security, will tell you they have HTTPS. And they're right. Your hosting platform enforces it on the rendering host, Experience Edge serves over HTTPS, every scan comes back green. Job done.


Except it isn't. HTTPS is table stakes. The real security story is everything that sits on top of it: security headers, and in particular, Content Security Policy.

Why Headless Changes the Picture

Security headers are HTTP response headers that tell the browser how to behave, what scripts can execute, what domains the page can connect to, whether HTTPS should be enforced. In a traditional Sitecore deployment you configured them in one place. In a headless setup, you have multiple response points:

Layer Who owns it
Rendering host (Next.js on FEaaS) You
Custom APIs and middleware You
Experience Edge (GraphQL + media) Sitecore
Third-party scripts and services Their headers, but your CSP

The rule: if it sends a response to a browser, it needs security headers. Most of those points are yours to configure.

The Header Checklist

Not all security headers carry the same weight. The table below ranks them by security benefit, based on Mozilla's Web Security Guidelines, alongside what we typically see on production Sitecore AI sites. The red flags are at the top: HSTS is almost always present but poorly configured, and CSP is either missing entirely or so permissive it offers no real protection.

Header Benefit Typically present?
HTTPS + Strict-Transport-Security MAXIMUM Yes, but often poorly configured
Content-Security-Policy HIGH Poor or missing
Cookies (Secure, HttpOnly, SameSite) HIGH Yes
CORS HIGH Yes
X-Frame-Options HIGH Yes (site dependent)
Permissions-Policy MEDIUM No
X-Content-Type-Options LOW No
Referrer-Policy LOW Yes

Note: Priority rankings based on Mozilla's Web Security Guidelines.

The two problems we see consistently are HSTS (poorly configured) and CSP (poor or missing). We'll focus on these two for the rest of this post because they're where the biggest gaps exist in practice.

HSTS: The Gap Between HTTPS and HTTPS Enforced

Every Sitecore headless site we review has HTTPS. But there's a difference between "HTTPS is available" and "HTTPS is properly enforced."

Without correctly configured HSTS, a first-time visitor's browser may start with an HTTP request. Your server redirects to HTTPS, but that initial request is already visible on the network. An attacker can intercept it and strip the HTTPS entirely. HSTS tells the browser to skip HTTP completely and go straight to HTTPS on every future visit.

Three parts of the HSTS header are commonly misconfigured or missing:

  • max-age needs to be at least 12 months (31536000 seconds). Shorter values mean the browser "forgets" to enforce HTTPS too quickly, reopening the window for downgrade attacks.
  • includeSubDomains extends the policy to all subdomains. Without it, your preview and staging environments are exposed even if your production domain is protected.
  • preload registers your domain with browser vendors so that HTTPS is enforced from the very first visit, before any request is made. Without it, that first HTTP request is still vulnerable. You can submit your domain at hstspreload.org, but be aware this is effectively permanent, so make sure all subdomains support HTTPS before you submit.
  • Mixed content from authors: This is not a HSTS attribute, but it is closely related. Adding upgrade-insecure-requests to your CSP will automatically rewrite any hardcoded http:// URLs to HTTPS, catching content that authors have linked insecurely.

The fix is a simple update in your next.config.js for headers configuration (or similar in your relevant framework):

Figure 1: Example Next.js config, consider a switch for local development

This applies the header to every route on your rendering host. If you're running custom APIs or middleware on separate hosts, they need the same header configured independently.

Content Security Policy: The Hard One

resources from. Scripts, stylesheets, images, fonts, API connections: if something tries to load from an unlisted domain, the browser blocks it. That's your primary XSS defense.

The difficulty in a Sitecore AI headless site is that the allowlist gets long fast. Experience Edge, Google Tag Manager (which loads scripts dynamically), your font provider, video embeds, A/B testing tools. Miss one and the browser blocks it. Three months later a content author embeds a YouTube video or marketing adds a tracking pixel. Neither domain is in the CSP, and the embed silently breaks. That's why CSP is the hard one. But "hard" is not a reason to skip it or fake it.

What Bad Looks Like

Here's a real CSP from a production Sitecore AI headless site:

This is a wide-open door with a "Secured" sign on it. Wildcard * everywhere, unsafe-inline and unsafe-eval in script-src. Any injected script will execute. No XSS protection whatsoever.

This happens because CSP gets treated as a compliance checkbox. Pen tests check that the header exists, not whether it's effective. The finding says "CSP is missing," so someone adds a permissive policy to clear it and moves on. The header is present. The protection is not.

Test your own: paste your CSP into Google's CSP Evaluator and see what it enforces.

The directives that matter

A CSP is made up of directives, each controlling a different type of resource. You don't need all of them, but the ones below are the most relevant for a Sitecore AI headless site.

Directive What it controls Why it matters in Sitecore AI
default-src Fallback for any directive you do not set explicitly Your safety net. Set this to 'self' so anything you forget to define is restricted by default
script-src Where JavaScript can load and execute from Your primary XSS defence. This is the one that stops injected scripts
style-src Where CSS can load from Google Fonts, GTM stylesheets, any external CSS
img-src Where images can load from Must include your Sitecore media CDN and any image optimization service
font-src Where fonts can load from Google Fonts, fonts.gstatic.com, custom font CDNs
connect-src Where JavaScript can make network requests, such as fetch, XHR and WebSocket Experience Edge GraphQL, analytics endpoints, any API your components call
frame-src What content can be loaded in iframes on your pages YouTube, Vimeo, marketing embeds. Set to 'none' if you do not use iframes
frame-ancestors Who is allowed to embed your site in an iframe Clickjacking protection. Set to 'none' for public sites. See the editing host note below
base-uri What URLs can appear in the HTML base tag Prevents base tag injection. Set to 'self'
form-action Where forms can submit to Controls form hijacking. Set to 'self' unless forms post to external services
object-src Plugin content like Flash or Java applets Set to 'none'. There is no reason to allow these in 2026
upgrade-insecure-requests Automatically rewrites http:// to https:// Catches content author hardcoded HTTP URLs. We mentioned this in the HSTS section

Not every site needs every directive. But default-src, script-src, and frame-ancestors should be on every implementation. They cover the highest-risk attack vectors: script injection, resource loading, and clickjacking.

A Starting Policy

Below is a restrictive starting CSP you can set in next.config.js or your headless framework. Note the header is Content-Security-Policy-Report-Only    , not Content-Security-Policy. This means violations are logged to the browser console but nothing is blocked. 

Deploy it like this first in non-production, then review the violations in the browser console to identify which sources need to be added.

This policy is deliberately tight. It will block your third-party scripts, your analytics, your external fonts, and probably your media CDN. That's the point. Start restrictive and open up deliberately during the audit phase, adding only the specific domains you need: your Experience Edge endpoint, Google Tag Manager, your font provider, your media CDN.

The alternative, starting wide open and hoping to tighten later, is how you end up with the wildcard CSP we showed earlier. Nobody ever goes back to tighten it.

Choosing The CSP Approach

There are three approaches to implementing CSP. Which one you choose depends on how your site renders and how strict you need to be.

  • Allowlist-based: List trusted domains in a static response header. Works with static site generation and CDN caching, so there's no performance penalty. The trade-off is that you may need 'unsafe-inline' for script-src depending on how your framework handles inline scripts, which weakens XSS protection. For content-driven sites where performance matters, this is the pragmatic starting point.

  • Nonce-based: A unique token is generated per request and embedded in both the CSP header and your script/style tags. Only scripts carrying the matching nonce are allowed to execute. This is the strongest option, but it forces every page to render dynamically. No static generation, no edge caching. For content-heavy marketing sites, that's a significant performance regression. For authenticated routes, dashboards, or API-driven pages, it's the right choice.

  • Hash-based (emerging): Cryptographic hashes of your scripts are generated at build time and added to the CSP header or as integrity attributes. This preserves static generation and CDN caching while avoiding 'unsafe-inline'. The best of both worlds in theory. In practice, framework support is still maturing and this isn't production-ready for most teams yet. Worth watching.

     

Which approach applies to which directives?

  • Allowlist-based applies to every directive. You're listing trusted domains for scripts, styles, images, fonts, connections, frames, everything.

  • Nonce-based applies to script-src and style-src only. These are the two directives where inline content is the risk. All other directives (img-src, font-src, connect-src, frame-src etc.) still use domain allowlists regardless.

  • Hash-based applies to script tags only. Hashes verify that a specific script file hasn't been modified. Other resource types still rely on domain allowlists.

In practice, this means even with a nonce-based or hash-based CSP, you're still maintaining an allowlist for images, fonts, API connections, and frames. The approach you choose only changes how you handle scripts and styles.

Recommendation: Use allowlist-based CSP for static content pages and nonce-based CSP for dynamically rendered routes like authentication flows and API endpoints. Most sites will start with allowlist-based and adopt nonces for specific routes as their CSP matures.

Next.js implementation: Next.js 16 fully supports allowlist-based and nonce-based CSP. Allowlist-based CSP is configured in next.config.js, nonce-based CSP is generated in proxy.ts with automatic nonce injection into framework scripts. Hash-based CSP is available as an experimental feature through Subresource Integrity (SRI) but should not be relied on for production use yet. Full implementation detail is in the Next.js CSP documentation.

The editing host

One thing that catches teams out: the Sitecore AI editing host runs the same Next.js application, but it's loaded within Sitecore's Pages editor. A restrictive CSP on the editing host, particularly frame-ancestors set to 'none', can break the editing experience.

This means you should consider a separate CSP configuration for the editing host. You may need to adjust frame-ancestors and other directives to accommodate the editing interface. Maintain this as a distinct configuration rather than loosening your public site's CSP to accommodate editing.

Deploying CSP: Four Phases

Whichever approach you choose, the rollout process is the same:

  1. Audit: catalogue every script, style, and connection your site makes, including content-authored pages. Check both the public site and the editing host.
  2. Report-Only: deploy with Content-Security-Policy-Report-Only. Violations are logged to the browser console but nothing is blocked. Run this for at least two weeks, covering a full content authoring cycle so you catch embeds and integrations that don't appear on every page.
  3. Tighten: review the violations. Add legitimate sources to your allowlist. Remove unnecessary sources. Fix inline script violations where possible. Repeat until the console is clean.
  4. Enforce: switch to Content-Security-Policy. Keep a report-to endpoint active so you catch regressions when someone adds a new third-party script or a content author embeds a new source.

The starting policy we showed earlier is designed for phase 2. Deploy it in Report-Only, see what needs adding, and carve out exactly what's needed. That's how you end up with a CSP that actually protects something.

The Minimum Bar

  1. HSTS with max-age=31536000; includeSubDomains; preload. One line in your next.config.js headers.
  2. A CSP that actually restricts something. Not a wildcard policy that exists to clear a pen test finding. Start with the restrictive policy we showed earlier and open it up deliberately.
  3. Deploy in Report-Only first. Never enforce a CSP you haven't observed in production traffic. Deploy, observe, tighten, then switch.

These aren't exotic hardening measures. The browsers support them; the tooling is there. The only thing missing from most production sites is the time to configure them properly.

Reference: Mozilla's Comprehensive Security Headers Guidance

Up next: Secrets and Tokens in Headless Sitecore

Security headers protect what the browser does. But they can't protect you from credentials that are already exposed. The next post tackles secrets and token management in headless Sitecore: why the credential surface expands in a Sitecore AI deployment, the three ways secrets leak (into the repository, the build pipeline, and the client-side bundle), and the controls that contain the damage when one does. If you've ever added NEXT_PUBLIC_ to a variable because you couldn't work out why it wasn't available in a component, that post is for you.