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.

August 28, 2025

By Sohrab Saboori

Why CORS for Static Files in Next.js Sitecore Projects Matters

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:

  • Secure static files (JS, CSS, fonts, images) served by Next.js or CDN
  • Enable API communication between your app and Sitecore (Layout Service, Media API, etc.)
  • Avoid wildcard policies () that fail security audits and penetration tests

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.

The Sitecore Challenge: Balancing Static Asset Security and CMS Integration

Dual CORS Requirements

In a Next.js + Sitecore setup, you must address CORS in two areas:

  1. App-to-Sitecore Communication
    • Layout Service API calls
    • Media item requests
    • Content management operations
  2. Static File Security
    • JavaScript chunks & ES modules (may require CORS if loaded cross-domain)
    • Fonts (browsers enforce CORS on fonts from another domain)
    • Images & CSS (only need CORS if accessed via JS APIs like fetch or <canvas>)
    • PWA assets (service workers, manifest, etc.)

The Plugin Ecosystem

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.

📋 Implementation Strategy

1. Audit Current Configuration

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

2. Three-Layer Approach

CORS in a Next.js + Sitecore project happens at three layers:

  1. Next.js Main Config → controls static files (/_next, fonts, PWA assets)
  2. CORS Plugin → controls Sitecore JSS integration (Layout Service, Media API)
  3. Sitecore API Key → controls backend API access (explicit origins in Sitecore)

Flowchart showing the process of securing static assets with Sitecore integration using Next.js config, CORS plugin, and Sitecore API key.

Image1: Each layer contributes headers or allowed origins, and together they form a unified CORS policy across Next.js static files and Sitecore APIs.

3. Priority System

// 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

Code Implementation

Step 0: Environment Configuration

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

Step 1: Update Main Next.js Configuration

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
        },
      ],
    },
  ];
}

Step 2: Update CORS Plugin for Sitecore (optional)

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;

Step 3: Update Sitecore API Key Configuration

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

Common Pitfalls & Solutions

1. Invalid Regex Patterns

❌ 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*',

2. Missing Environment Variables

❌ 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 || '',
}

3. CORS Header Conflicts

❌ 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;

4. Overly Restrictive CORS

❌ 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;

Testing & Validation

1. Browser Developer Tools

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")
  );
});

2. curl Commands

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

🎯 Best Practices

1. Environment-Specific Configuration

// 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;
};

2. Organized Route Patterns

// 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",
    },
  ],
}));

3. Monitoring and Logging

// 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();
};

4. Documentation and Maintenance

/**
 * 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
 */

Bringing It All Together: A Unified CORS Policy for Next.js + Sitecore

Implementing CORS in a Next.js + Sitecore project isn’t just about flipping a header — it’s about coordinating three layers:

  • Next.js config for static assets and fonts
  • Sitecore JSS CORS plugin for Layout Service and media APIs
  • Sitecore API Key origins for backend access

When aligned, these give you a unified CORS policy that protects assets, secures CMS communication, and passes security audits without breaking functionality.

Key takeaways:

  1. Avoid wildcards — always use explicit origins.
  2. Handle static CORS with simple, fixed headers; handle API CORS dynamically with preflight support.
  3. Use Sitecore serialization to manage API Key CORS origins consistently across environments.
  4. Add hotlink protection (Media Request Protection, CDN rules, middleware) if bandwidth theft is a concern.
  5. Test with browser dev tools, 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.*

Photo of Fishtank employee Sohrab Saboori

Sohrab Saboori

Senior Full-Stack Developer

Sohrab is a Senior Front-End Developer with extensive experience in React, Next.js, JavaScript, and TypeScript. Sohrab is committed to delivering outstanding digital solutions that not only meet but exceed clients' expectations. His expertise in building scalable and efficient web applications, responsive websites, and e-commerce platforms is unparalleled. Sohrab has a keen eye for detail and a passion for creating seamless user experiences. He is a problem-solver at heart and enjoys working with clients to find innovative solutions to their digital needs. When he's not coding, you can find him lifting weights at the gym, pounding the pavement on the run, exploring the great outdoors, or trying new restaurants and cuisines. Sohrab believes in a healthy and balanced lifestyle and finds that these activities help fuel his creativity and problem-solving skills.