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.
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.
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.
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:
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.
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:
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.
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
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.