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.js
MiddlewareStatic 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
andframe-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-requests
Data 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.