A Comprehensive Guide to Securing Static Assets while Maintaining Sitecore Integration
Mastering CORS in Next.js + Sitecore ensures secure static assets, safe CMS integration, and audit-ready deployments—set it up right the first time.
Start typing to search...
Mastering CORS in Next.js + Sitecore ensures secure static assets, safe CMS integration, and audit-ready deployments—set it up right the first time.
Cross-Origin Resource Sharing (CORS) defines whether browser JavaScript can read responses from another domain. In Next.js + Sitecore projects, getting CORS right is critical because you must:
Note: CORS only governs whether a response is exposed to browser JavaScript. It does not stop hotlinking (other sites embedding your images or scripts). To prevent that, you need CDN rules, signed URLs, or middleware.
In a Next.js + Sitecore setup, you must address CORS in two areas:
fetch or <canvas>)Sitecore JSS includes a CORS header plugin that handles CMS integration:
// src/lib/next-config/plugins/cors-header.js
const corsHeaderPlugin = (nextConfig = {}) => {
return Object.assign({}, nextConfig, {
async headers() {
return [
{
source: "/_next/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: config.sitecoreApiHost, // Could be wildcard!
},
],
},
];
},
});
};
The problem: This plugin might use wildcards or overly permissive origins.
First, identify all CORS configurations in your project:
# Search for CORS configurations
grep -r "Access-Control-Allow-Origin" src/
grep -r "cors" src/
grep -r "\\*" src/ | grep -i cors
CORS in a Next.js + Sitecore project happens at three layers:

Image1: Each layer contributes headers or allowed origins, and together they form a unified CORS policy across Next.js static files and Sitecore APIs.
// Priority order for allowed origins:
1. process.env.NEXTAUTH_URL // App-specific domain
2. config.sitecoreApiHost // Sitecore CMS domain
3. '<https://app.domain.com>' // Fallback production domain
Add environment-specific app origins.
.env.local / .env.qa / .env.production
# Environment-specific CORS origins
NEXTAUTH_URL=https://app-d.domain.com # Development
# NEXTAUTH_URL=https://app-q.domain.com # QA
# NEXTAUTH_URL=https://app.domain.com # Production
# Ensure required environment variables have fallbacks
ANYLINE_OCR_LICENSE_KEY=your-license-key-or-empty
LIVECHAT_LICENSE_KEY=your-license-key-or-empty
File: next.config.js
// Security headers for Vercel conformance
async headers() {
return [
// API endpoints - Restrictive CORS
{
source: '/api/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: process.env.NEXTAUTH_URL || '<https://app.domain.com>',
},
{
key: 'Access-Control-Allow-Credentials',
value: 'true',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, POST, PUT, DELETE, OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value: 'Content-Type, Authorization, X-Requested-With',
},
],
},
// Next.js static assets - Secure CORS
{
source: '/_next/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: process.env.NEXTAUTH_URL || '<https://app.domain.com>',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, HEAD, OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value: 'Content-Type, Cache-Control',
},
],
},
// Public directory assets - Organized by type
{
source: '/images/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: process.env.NEXTAUTH_URL || '<https://app.domain.com>',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, HEAD, OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value: 'Content-Type, Cache-Control',
},
],
},
{
source: '/icons/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: process.env.NEXTAUTH_URL || '<https://app.domain.com>',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, HEAD, OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value: 'Content-Type, Cache-Control',
},
],
},
{
source: '/fonts/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: process.env.NEXTAUTH_URL || '<https://app.domain.com>',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, HEAD, OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value: 'Content-Type, Cache-Control',
},
],
},
// Global security headers
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' <https://trusted-domains.com>",
"style-src 'self' 'unsafe-inline' <https://fonts.googleapis.com>",
"font-src 'self' <https://fonts.gstatic.com>",
"img-src 'self' data: blob: https: http:",
"connect-src 'self' https: wss:",
"frame-src 'self' <https://trusted-domains.com>",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
].join('; '),
},
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload',
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Server',
value: '', // Hide server information
},
{
key: 'X-Powered-By',
value: '', // Hide technology stack
},
],
},
];
}
File: src/lib/next-config/plugins/cors-header.js
const config = require("../../../temp/config");
/**
* Enhanced CORS plugin for Sitecore integration
* Prioritizes security while maintaining CMS functionality
* @param {import('next').NextConfig} nextConfig
*/
const corsHeaderPlugin = (nextConfig = {}) => {
if (!config.sitecoreApiHost) {
return nextConfig;
}
// Multi-level fallback for allowed origins
const allowedOrigin =
process.env.NEXTAUTH_URL || // 1. App domain (highest priority)
config.sitecoreApiHost.replace(/\\/$/, "") || // 2. Sitecore CMS domain
"<https://yourapp.com>"; // 3. Production fallback
return Object.assign({}, nextConfig, {
async headers() {
const extendHeaders =
typeof nextConfig.headers === "function"
? await nextConfig.headers()
: [];
return [
...(await extendHeaders),
{
source: "/_next/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: allowedOrigin,
},
{
key: "Access-Control-Allow-Methods",
value: "GET, HEAD, OPTIONS",
},
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Cache-Control",
},
],
},
{
source: "/api/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: allowedOrigin,
},
{
key: "Access-Control-Allow-Credentials",
value: "true",
},
{
key: "Access-Control-Allow-Methods",
value: "GET, POST, PUT, DELETE, OPTIONS",
},
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization, X-Requested-With",
},
],
},
];
},
});
};
module.exports = corsHeaderPlugin;
File: src/PWA.Serialization/serialization/APIKey/API Keys/www.yml
SharedFields:
- Hint: CORS Origins
Value: https://app.domain.com,https://app-q.domain.com,https://app-d.domain.com
❌ Problem:
// This will cause Next.js to fail
source: '/((?!api/|_next/|healthz|sitecore/api/|-/).*\\\\.(js|css|png))',
Error:
Error parsing regex: Capturing groups are not allowed at position 46
✅ Solution:
// Use simple path patterns instead
source: '/images/:path*',
source: '/fonts/:path*',
source: '/icons/:path*',
❌ Problem:
env: {
ANYLINE_OCR_LICENSE_KEY: process.env.ANYLINE_OCR_LICENSE_KEY, // undefined = error
}
✅ Solution:
env: {
ANYLINE_OCR_LICENSE_KEY: process.env.ANYLINE_OCR_LICENSE_KEY || '',
LIVECHAT_LICENSE_KEY: process.env.LIVECHAT_LICENSE_KEY || '',
}
❌ Problem:
// Main config and plugin both setting headers for same route
// Can cause unpredictable behavior
✅ Solution:
// Ensure consistent priority order in both configurations
const allowedOrigin = process.env.NEXTAUTH_URL || fallback;
❌ Problem:
// Blocking legitimate Sitecore requests
{
key: 'Access-Control-Allow-Origin',
value: '<https://app.domain.com>', // Too restrictive for CMS
}
✅ Solution:
// Allow both app and CMS domains
const allowedOrigin = process.env.NEXTAUTH_URL || config.sitecoreApiHost;
Test CORS Headers:
// In browser console
fetch("<https://yourapp.com/_next/static/chunks/main.js>", {
method: "GET",
mode: "cors",
}).then((response) => {
console.log(
"CORS Headers:",
response.headers.get("Access-Control-Allow-Origin")
);
});
Test Static File CORS:
# Test image CORS
curl -H "Origin: <https://malicious-site.com>" \\
-H "Access-Control-Request-Method: GET" \\
-H "Access-Control-Request-Headers: X-Requested-With" \\
-X OPTIONS \\
<https://yourapp.com/images/logo.png>
# Should return 403 or no CORS headers
Test Legitimate Access:
# Test from allowed origin
curl -H "Origin: <https://app.domain.com>" \\
-H "Access-Control-Request-Method: GET" \\
-X OPTIONS \\
<https://yourapp.com/images/logo.png>
# Should return proper CORS headers
// Use environment variables for flexibility
const getCorsOrigin = () => {
const env = process.env.NODE_ENV || "development";
const origins = {
development: "<https://app-d.domain.com>",
staging: "<https://app-q.domain.com>",
production: "<https://app.domain.com>",
};
return process.env.NEXTAUTH_URL || origins[env] || origins.production;
};
// Group related static assets
const staticFileRoutes = [
{ source: "/images/:path*", type: "media" },
{ source: "/icons/:path*", type: "media" },
{ source: "/fonts/:path*", type: "fonts" },
{ source: "/_next/:path*", type: "build" },
];
const corsHeaders = staticFileRoutes.map((route) => ({
source: route.source,
headers: [
{
key: "Access-Control-Allow-Origin",
value: getCorsOrigin(),
},
{
key: "Access-Control-Allow-Methods",
value: route.type === "build" ? "GET, HEAD, OPTIONS" : "GET, HEAD",
},
],
}));
// Add CORS logging middleware
const corsLogger = (req, res, next) => {
const origin = req.headers.origin;
const allowedOrigin = getCorsOrigin();
if (origin && origin !== allowedOrigin) {
console.warn(`CORS: Blocked request from ${origin} to ${req.path}`);
}
next();
};
/**
* CORS Configuration for Static Files
*
* Purpose: Secure static assets while maintaining Sitecore integration
*
* Routes covered:
* - /_next/:path* (Next.js build assets)
* - /images/:path* (Public images)
* - /icons/:path* (App icons)
* - /fonts/:path* (Web fonts)
*
* Allowed origins:
* - Development: <https://app-d.domain.com>
* - QA: <https://app-q.domain.com>
* - Production: <https://app.domain.com>
*
* Last updated: December 2024
* Next review: March 2025
*/
Implementing CORS in a Next.js + Sitecore project isn’t just about flipping a header — it’s about coordinating three layers:
When aligned, these give you a unified CORS policy that protects assets, secures CMS communication, and passes security audits without breaking functionality.
Key takeaways:
curl, and automated tests to confirm the right headers are applied.By following this layered approach, your project will have a secure, predictable, and maintainable CORS configuration — one that protects static assets and Sitecore APIs while keeping your Next.js PWA fast and reliable.
*Pro Tip: Regular security audits should include CORS policy reviews to ensure your configuration remains secure as your application evolves.*