content security policy csp nextjs

How to Secure Your Next.js App with a Robust Content Security Policy (CSP): The Shipnext Approach

How to Secure Your Next.js App with a Robust Content Security Policy (CSP): The Shipnext Approach

shipnext

Launching a modern SaaS or web product with Next.js? Security isn’t optional — it’s your first layer of trust. One of the most overlooked yet powerful defenses against attacks like Cross-Site Scripting (XSS) is the Content Security Policy (CSP).

At Shipnext, where we build powerful, no-code-enabled SaaS infrastructure, we’ve implemented a strict CSP that balances security, developer flexibility, and third-party integrations like Stripe, Google Meet, and Brevo. Here's how we do it — and how you can too.

What is a Content Security Policy?

A Content Security Policy is an HTTP header (or meta tag) that tells the browser which resources (scripts, styles, images, etc.) it’s allowed to load and execute.

Think of it as a firewall for your frontend. It protects your users against:

  • XSS (Cross-Site Scripting)

  • Data injection attacks

  • Malicious third-party code

How to Implement CSP in Next.js

There are two main ways to implement a Content Security Policy in a Next.js app:

1. Via next.config.js (Static Headers)

This method adds headers to every route at the server level using Next.js’s built-in support for custom headers.

// next.config.js
const ContentSecurityPolicy = `
  default-src 'self';
  script-src 'self' <https://js.stripe.com> 'unsafe-eval';
  style-src 'self' 'unsafe-inline' <https://fonts.googleapis.com>;
  img-src * blob: data:;
  font-src 'self' <https://fonts.gstatic.com>;
  connect-src 'self' <https://api.brevo.com> <https://meet.google.com>;
  frame-src <https://js.stripe.com> <https://meet.google.com>;
`;

module.exports = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          {
            key: "Content-Security-Policy",
            value: ContentSecurityPolicy.replace(/\\n/g, ""),
          },
        ],
      },
    ];
  },
};

2. Via Middleware (Dynamic Headers)

If you want per-route CSP, user-based CSP, or to inject nonces dynamically, use Next.js Middleware (Edge functions). This is ideal for advanced use cases.

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

export function middleware(req: NextRequest) {
  const res = NextResponse.next();

  const csp = `
    default-src 'self';
    script-src 'self' <https://js.stripe.com> 'unsafe-eval';
    style-src 'self' 'unsafe-inline' <https://fonts.googleapis.com>;
    font-src 'self' <https://fonts.gstatic.com>;
    img-src * blob: data:;
    connect-src 'self' <https://api.brevo.com> <https://meet.google.com>;
    frame-src <https://js.stripe.com> <https://meet.google.com>;
  `.replace(/\\n/g, "");

  res.headers.set("Content-Security-Policy", csp);

  return res;
}

Don't forget to enable middleware in middleware.ts and ensure it applies to your routes.

next.config.js vs Middleware for CSP

Featurenext.config.jsMiddlewareStatic headers Yes NoDynamic content (nonces, user-specific rules) No YesPerformance Fast️ Slightly slower (runs on Edge Runtime)Granular control per route No YesEasier to maintain Yes️ More complexBest for production Simple cases Advanced use cases

When to Use Middleware

Use middleware when:

  • You need per-request CSPs (e.g. nonces for inline scripts)

  • Your app has authenticated areas with different policies

  • You're embedding dynamic third-party scripts conditionally

CSP Implementation in Shipnext

At Shipnext, we provide a boilerplate for fast SaaS deployment that includes features like e-commerce, marketplace, blog, landing page and event and appointment booking.

We integrate multiple third-party services securely:

  • Stripe → Payments (allows script-src and frame-src)

  • Brevo (Sendinblue) → CRM and chatbot (requires frame-src)

  • CloudFront / S3 → Asset delivery (img-src blob/data/https)

At Shipnext, our goal is to combine speed and simplicity with real-world security. That’s why our Content Security Policy is modular, environment-aware, and enforced at the middleware level using centralized constants.

Instead of hardcoding the CSP in the middleware or config, we define everything in a dedicated security.constants.ts file — keeping logic clean and reusable.

security.constants.ts: Define Once, Use Everywhere

export const securityEnabled = true;

export const ALLOWED_ORIGIN =
  "<https://conversations-widget.brevo.com/,http://localhost:3000,https://www.shipnex,www.shipnext.biz,shipnext.biz,cloudflare>";

export const scriptSources = [
  "'self'",
  "<https://api.stripe.com/v1/products>",
  "<https://conversations-widget.brevo.com/>",
  "<https://google.com>",
  ...(isDevelopment ? ["'unsafe-eval'"] : []),
];

export const styleSources = [
  "'self'",
  "'unsafe-inline'",
  ...(isDevelopment ? ["*"] : []),
];

export const imgSources = [
  "'self'",
  "blob:",
  "data:",
  "<https://www.googletagmanager.com>",
  "<https://analytics.google.com>",
  ...(isDevelopment ? ["*"] : []),
];

export const connectSources = [
  "'self'",
  "<https://www.googletagmanager.com>",
  "<https://analytics.google.com>",
  "<https://api.iconify.design>",
  "<https://api.unisvg.com>",
  "<https://api.simplesvg.com>",
  "<https://api.stripe.com/v1/products>",
  "<https://api.stripe.com/v1/prices>",
  "<https://api.stripe.com/v1/checkout/sessions>",
  "<https://conversations-widget.brevo.com/>",
  ...(isDevelopment ? ["*"] : []),
];

export const cspHeader = `
  default-src 'self';
  script-src ${scriptSources.join(" ")};
  style-src ${styleSources.join(" ")};
  img-src ${imgSources.join(" ")};
  connect-src ${connectSources.join(" ")};
  font-src 'self' ${isDevelopment ? "* data:" : ""};
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  frame-src 'self' <https://conversations-widget.brevo.com/>;
  ${!isDevelopment ? "upgrade-insecure-requests;" : ""}
`
  .replace(/\\s{2,}/g, " ")
  .trim();

This setup allows you to:

  • Add or remove allowed domains in one place

  • Dynamically inject environment-specific rules

  • Avoid mistakes across multiple files

middleware.ts: Enforcing Security Headers

We use Next.js middleware to intercept every request and apply CSP and other security headers conditionally:

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const response = NextResponse.next();

  // Static and admin routes handling skipped for brevity...

  const origin = request.headers.get("origin") || "";
  const referer = request.headers.get("Referer") ?? "";
  const isLocalhostRequest = isLocalhost(origin) || isLocalhost(referer);
  const finalOrigin = isLocalhostRequest && !isDevelopment
    ? process.env.NEXT_PUBLIC_FRONTEND_URL || origin
    : origin;

  // Security headers
  if (securityEnabled && !isDevelopment) {
    if (!corsOptions.allowedOrigins.includes(finalOrigin)) {
      return NextResponse.json({ error: "Forbidden" }, { status: 403 });
    }

    if (request.method === "OPTIONS") {
      return handleOptionsRequest(response);
    }

    // Apply security headers
    setSecurityHeaders(response, finalOrigin);
  }

  return response;
}

This gives us:

  • Central control over origin and CSP logic

  • Dynamic behavior based on dev/production

  • Flexibility for more advanced logic (e.g., affiliate tracking, locale, caching)

Why This Setup Works

Shipnext’s implementation helps prevent:

VulnerabilityHow CSP + Middleware HelpsXSSDisallows inline/untrusted scriptsFormjackWhitelists only required Stripe/Brevo APIsClickjackingframe-ancestors 'none'Mixed Contentupgrade-insecure-requestsData LeaksControlled connect-src only to trusted APIs

By combining strict CSP, dynamic origin checks, and clean code separation, Shipnext ensures that every page, every route, and every user interaction stays safe — whether it's Stripe checkout, CRM interaction, or appointment scheduling.

Testing & Debugging CSP

To test and refine your CSP, we recommend:

  • CSP Evaluator – Google tool to audit your policy

  • Chrome DevTools → Console tab shows blocked requests

  • securityheaders.com – Check all your HTTP security headers

  • Add a report-uri to log violations:

Content-Security-Policy: default-src 'self'; report-uri /csp-report

Real Error: Brevo Script Blocked

Error Message:

Fix: Add the missing domain to script-src:

export const scriptSources = [
  "'self'",
  "<https://conversations-widget.brevo.com/>",
  ...
];

Real Error: Stripe API Blocked

Error Message:

Refused to connect to '<https://api.stripe.com/v1/prices>' because it violates the
following Content Security Policy directive: "connect-src 'self' ..."

Fix: Add missing Stripe endpoints to connect-src:

export const connectSources = [
  "'self'",
  "<https://api.stripe.com/v1/prices>",
  "<https://api.stripe.com/v1/products>",
  "<https://api.stripe.com/v1/checkout/sessions>",
  ...
];

How to Test CSP Effectively (Checklist)

Here’s a step-by-step guide to test your CSP setup properly:

1. Use Chrome DevTools

  • Open Console → Filter by "CSP"

  • Watch for blocked requests, domains, or unsafe inline errors

While CSP is a security feature, it also indirectly improves SEO by:

  • Boosting trust and brand reputation

  • Helping with GDPR/PCI compliance

  • Ensuring better Core Web Vitals by preventing malicious script injections

  • Making your app more indexable by avoiding blocked assets

Search engines favor secure, fast, well-structured sites. Implementing CSP shows your site is serious about protecting users.

Best Practices for CSP in SaaS & Next.js

  • Whitelist only what you need — don’t go broad.

  • Use nonce or hash instead of 'unsafe-inline' when possible.

  • Avoid eval() or 'unsafe-eval' — it’s a common attack vector.

  • Test before deploying to prod — avoid breaking assets.

  • Use headers over meta — HTTP headers are more secure.

  • Conclusion

    Our CSP setup is built to support modern SaaS features like payments, email marketing, and video conferencing — all while staying safe.

    Whether you're building a landing page, marketplace, or full-stack SaaS, don’t skip on CSP. It’s one of the easiest and most powerful steps you can take to secure your Next.js project.

    Stay Ahead. Learn. Ship. Grow.

    Get tips on dev, marketing, and shipping real projects — plus updates, new features, and tools straight from the Shipnext stack.

    No spam. Just value.Includes free access to Shipnext Academy – learn how to build and sell like a pro.