February 8, 2026HeaderTest Team177 views

Next.js + Sanity CMS: The CSP Gap and the _next/image Proxy Problem

A lot of Next.js sites using Sanity CMS ship with no CSP at all. Worse, the _next/image endpoint can become an open image proxy for cdn.sanity.io. Here's what's happening and how to lock it down.

The Setup We Keep Seeing

We scan a lot of Next.js sites that use Sanity CMS. It's a popular stack, and almost none of them have a Content Security Policy.

That's not entirely surprising. Next.js doesn't add CSP by default. Sanity's docs don't push for it. Most Vercel deployment guides don't mention security headers at all. So teams ship without one and never circle back.

But there's a second problem in these setups that gets even less attention: the /_next/image endpoint. It's meant for image optimization, but the way most Sanity projects configure it, it becomes an open proxy for anything on cdn.sanity.io — not just your images.

We'll cover both problems and how to fix them. If you're not familiar with CSP basics, read our complete guide to Content Security Policy first — this post assumes you know what a directive is.

No CSP: What's Actually at Risk

A typical Sanity-powered site loads from a handful of external origins:

  • cdn.sanity.io — images and file assets
  • api.sanity.io — GROQ query API
  • apicdn.sanity.io — cached API responses

These are legitimate. Your site needs them. But without CSP, they're not explicitly allowlisted — they're implicitly trusted along with every other origin on the internet. There's a real difference between "we allow cdn.sanity.io" and "we allow anything."

This gets more interesting with how Sanity handles content. Rich text blocks can include embedded content, custom components, and portable text serializers. If any rendering path has a gap — an unsanitized field, a misconfigured serializer, a third-party embed — there's no CSP to catch whatever gets injected. The browser just runs it.

The _next/image Problem

Next.js has a built-in image optimization API at /_next/image. You use the <Image> component, Next.js fetches the original server-side, resizes it, and serves the optimized version. Good for performance, good for Core Web Vitals.

To allow external images, you configure remotePatterns in your config:

// next.config.js — typical Sanity setup
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.sanity.io',
      },
    ],
  },
};

This tells Next.js: allow optimization for any URL on cdn.sanity.io. And that's the problem. It allows any image from that hostname — not just your project's images.

So this works as expected:

/_next/image?url=https://cdn.sanity.io/images/YOUR_PROJECT/production/abc123.png&w=800&q=75

But so does this:

/_next/image?url=https://cdn.sanity.io/images/SOMEONE_ELSES_PROJECT/production/xyz789.png&w=800&q=75

Any image on cdn.sanity.io — any project, any dataset — gets proxied through your domain. Your server fetches it, processes it, serves it as if it's yours.

What This Actually Costs You

An open image proxy sounds abstract until you think about what happens in practice:

  • Bandwidth. Someone discovers your endpoint and starts routing arbitrary Sanity CDN images through it. You pay for the egress.
  • CPU and memory. Every /_next/image request triggers a fetch, decode, resize, and re-encode. Image processing is expensive. Enough requests and your actual users start noticing.
  • Cache poisoning. If Cloudflare or Vercel's edge network sits in front, proxied images get cached there. Now your CDN is serving content you never asked for, tied to your domain.
  • No second line of defense. Without img-src in CSP, there's nothing stopping a browser from loading images through the proxy even if they were injected via XSS. CSP would catch this — but you don't have one.
  • Your domain, someone else's content. Whatever is on the other end of that Sanity CDN URL, your domain is now serving it. That's a liability.

Locking Down _next/image

The fix is in next.config.js. Instead of allowing all of cdn.sanity.io, restrict the pathname to your specific project ID and dataset:

// next.config.js — locked down
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.sanity.io',
        pathname: '/images/YOUR_PROJECT_ID/production/**',
      },
    ],
    minimumCacheTTL: 3600,
  },
};

Swap in your actual project ID. If you use multiple datasets, add a pattern for each. Now /_next/image rejects anything that doesn't match — other projects' images get a 400.

A few more things worth doing at the infrastructure level:

  • Rate limit the /_next/image endpoint at your reverse proxy. Legitimate traffic to this endpoint is predictable. A spike isn't.
  • Set minimumCacheTTL (shown above) so the same image doesn't get re-processed on every request. 3600 is a reasonable starting point.
  • Watch your server metrics. Unusual CPU or memory usage correlated with /_next/image traffic is worth investigating.

Building a CSP for Next.js + Sanity

Here's a starter policy. Each directive is scoped to what this stack actually needs:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' cdn.sanity.io data:;
  font-src 'self';
  connect-src 'self' api.sanity.io apicdn.sanity.io;
  media-src 'self' cdn.sanity.io;
  frame-src 'none';
  object-src 'none';
  base-uri 'self';
  form-action 'self';

What each one does:

  • default-src 'self' — the baseline. Only your origin unless overridden.
  • script-src 'self' 'nonce-{RANDOM}' — your scripts plus anything with a valid nonce. No 'unsafe-inline', no 'unsafe-eval'. See our guide to fixing unsafe-inline for the full nonce setup.
  • style-src 'self' 'unsafe-inline' — a pragmatic starting point. CSS-in-JS and component libraries love inline styles. Tighten this later with nonces or hashes once you're stable.
  • img-src 'self' cdn.sanity.io data: — your images plus Sanity CDN. The data: is for base64 LQIP placeholders if you use them.
  • connect-src 'self' api.sanity.io apicdn.sanity.io — GROQ queries and Sanity API calls.
  • frame-src 'none' and object-src 'none' — shut these down unless you have a specific reason not to.

Deploy this as Content-Security-Policy-Report-Only first. It logs violations without blocking anything. Run it for a week or two, fix what breaks, then switch to enforcing.

The Middleware Approach

In Next.js, the cleanest way to add CSP is through middleware. Here's a minimal version that generates a per-request nonce:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');

  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}'`,
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' cdn.sanity.io data:",
    "font-src 'self'",
    "connect-src 'self' api.sanity.io apicdn.sanity.io",
    "frame-src 'none'",
    "object-src 'none'",
  ].join('; ');

  const response = NextResponse.next();
  response.headers.set('Content-Security-Policy', csp);
  response.headers.set('x-nonce', nonce);
  return response;
}

Then read the nonce in your layout so you can pass it to scripts:

// app/layout.tsx
import { headers } from 'next/headers';

export default function RootLayout({ children }) {
  const nonce = headers().get('x-nonce') || '';

  return (
    <html>
      <body>
        <Script nonce={nonce} src="/analytics.js" />
        {children}
      </body>
    </html>
  );
}

Fresh nonce on every request. Any script without the matching nonce attribute gets blocked.

Quick Checklist

  • Restrict remotePatterns in next.config.js to your specific project ID and dataset path
  • Add minimumCacheTTL to avoid reprocessing the same images
  • Rate limit /_next/image at the reverse proxy level
  • Deploy a CSP in Report-Only mode first
  • Scope img-src, connect-src, and script-src to the origins you actually use
  • Use nonces for script-src instead of 'unsafe-inline'
  • Monitor violation reports for unexpected loads
  • Run your site through HeaderTest to see where your headers stand

Topics

next.js cspsanity cms security_next/image proxycontent security policynext.js security headerssanity cdnimage optimization security

Check Your Website's Security

Use our free scanner to analyze your CSP and security headers.

Scan Now - Free