Adding Content-Security-Policy (CSP) and Strict-Transport-Security (HSTS) headers to your Next.js application is a critical step in hardening its security posture against common web vulnerabilities.

Let’s see this in action with a simple Next.js app. Imagine a page that loads an external script. Without proper CSP, this script could be anything, even malicious.

// pages/index.js
export default function HomePage() {
  return (
    <div>
      <h1>Welcome!</h1>
      <p>This page loads an external script.</p>
      <script src="https://example.com/innocent.js"></script>
    </div>
  );
}

Now, let’s add the security headers. In Next.js, the most straightforward way to do this is by creating a custom next.config.js file.

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)', // Apply to all routes
        headers: [
          // Content Security Policy
          {
            key: 'Content-Security-Policy',
            value: "default-src 'self'; script-src 'self' https://example.com; style-src 'self'; img-src 'self' data:; connect-src 'self';",
          },
          // Strict Transport Security
          {
            key: 'Strict-Transport-Security',
            value: 'max-age=31536000; includeSubDomains; preload',
          },
        ],
      },
    ];
  },
};

After restarting your Next.js development server (npm run dev or yarn dev) and visiting your application, you can inspect the response headers using your browser’s developer tools (usually under the "Network" tab, then select the main document request and look for "Response Headers"). You’ll see Content-Security-Policy and Strict-Transport-Security listed.

The Content-Security-Policy header is your primary defense against cross-site scripting (XSS) and data injection attacks. It acts as a whitelist, instructing the browser on which sources of content (scripts, styles, images, etc.) are allowed to be loaded and executed. In the example above:

  • default-src 'self': By default, only content from the same origin as the document is allowed.
  • script-src 'self' https://example.com: Scripts can only be loaded from the same origin or https://example.com. If https://example.com/innocent.js were replaced with a malicious script hosted elsewhere, the browser would refuse to load it, preventing the attack.
  • style-src 'self': Stylesheets are only allowed from the same origin.
  • img-src 'self' data:: Images can be loaded from the same origin or embedded as data: URIs.
  • connect-src 'self': Connections (like fetch or XMLHttpRequest) are restricted to the same origin.

This granular control allows you to precisely define your application’s trusted content sources. The Strict-Transport-Security (HSTS) header is simpler but equally crucial. It tells browsers to only communicate with your site over HTTPS for a specified period.

  • max-age=31536000: This is the duration (in seconds) the browser should enforce HTTPS. Here, it’s one year.
  • includeSubDomains: This directive ensures that all subdomains of your site also enforce HTTPS.
  • preload: This is an opt-in mechanism for browsers to include your site in a hardcoded list of HSTS-enabled sites, meaning even the very first visit will be over HTTPS. You typically need to submit your domain to HSTS preload lists (like the one maintained by Chromium) for this to take effect.

The primary benefit of HSTS is preventing "protocol downgrade attacks" and "cookie hijacking" over unencrypted connections. Once a browser has seen the HSTS header from your site, it will automatically convert any attempts to access your site via HTTP to HTTPS before sending the request.

The exact levers you control are the directives within the Content-Security-Policy and the max-age and includeSubDomains values in Strict-Transport-Security. Carefully crafting your CSP is an iterative process. Start with the most restrictive policy that allows your application to function, and then gradually loosen it as needed. Overly broad policies can break legitimate functionality. For instance, if your application dynamically loads scripts from a CDN, you’ll need to explicitly add that CDN’s domain to your script-src.

A common pitfall when configuring CSP is forgetting to allow unsafe-inline or unsafe-eval for inline scripts/styles or eval() calls if your application genuinely relies on them. However, it’s always best practice to migrate away from these if possible, by moving inline scripts to separate .js files and using standard DOM manipulation instead of eval().

The next security consideration you’ll likely encounter after implementing CSP and HSTS is managing Cross-Origin Resource Sharing (CORS) policies, especially if your Next.js application serves as an API backend.

Want structured learning?

Take the full Nextjs course →