Adding Security Headers to a Next.js Application


I’ve recently been hacking away trying to add new features to shrtli.com, my URL-shortening web app. The project started in 2021 I believe after learning full stack MERN development. Recently, I’ve addded a couple of new features to the API including:

The only catch is all of these features are yet to be implemented on the front end. Instead of doing that, I decided to embark on a dreadful security journey to try and chase the A+ score from SecurityHeaders.com.

In a nutshell, that includes setting several security headers on your website, including **x-content-type-options**, **x-frame-options**, **referrer-policy**, **permissions-policy** (a modern replacement to **feature-policy**), and, lastly, the elephant in the room, content-security-policy(or csp for short).

Before the implementation, shrtli.com scored a D grade. Some of the issues outlined by Security Headers for improvement were setting all the headers specified above. That journey hasn’t been easy, to say the least.

First, Next.js 14 doesn’t have enough documentation for setting security headers in its app directory, or at least it felt like that to me. There’s a page dedicated that to configuring CSP in Next.js which I can only hope everyone understands and I don’t.

Let me give you a simple rundown of how it went while juggling lots of resources online in order to successfully get a near-perfect grade of A from D (and why I didn’t go for A+).

The Beginning

I first began by following the documentation, which advices setting a CSP in middleware.ts like so:

// middleware.ts
import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data:;
    font-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    block-all-mixed-content;
    upgrade-insecure-requests;
`;
  // Replace newline characters and spaces
  const contentSecurityPolicyHeaderValue = cspHeader
    .replace(/\s{2,}/g, " ")
    .trim();

  // Clone the request headers
  const requestHeaders = new Headers(request.headers);
  // add a custom request header to read the nonce value
  requestHeaders.set("x-nonce", nonce);

  requestHeaders.set(
    "Content-Security-Policy",
    contentSecurityPolicyHeaderValue
  );

  // Create new response
  const response = NextResponse.next({
    request: {
      // parse the new request headers
      headers: requestHeaders,
    },
  });
  // Also set the CSP header in the response so that it is outputted to the browser
  response.headers.set(
    "Content-Security-Policy",
    contentSecurityPolicyHeaderValue
  );

  return response;
}

Of course, Next.js middleware runs on all routes so setting headers in this file is perfect. Set up once and forget about it. If that’s not what you want, there’s a matcher array in the configuration that you can use to fine tune where middleware should and shouldn’t run:

// middleware.ts
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    {
      source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
      missing: [
        { type: "header", key: "next-router-prefetch" },
        { type: "header", key: "purpose", value: "prefetch" },
      ],
    },
  ],
};

Then once you’re done, you can read the nonce (number only once) in any server component by importing headers from next/headers followed by the following:

const nonce = headers().get("x-nonce");

I tried this, deployed the app and it wasn’t successful. I’ll largely blame my project structure though because there are some key differences with the strict CSP GitHub repository referenced by the docs.

However, Next.js docs also recommends an alternative to the method above by setting the CSP header in next.config.js but only “for applications that do not require nonces.”

After lots of “duckduckgoing” (searching for information on Duck Duck Go), I stumbled across Kieran Roberts’ article on how to set security headers in Next.js. The article hyighlights how you8 can set security headers in the special Next.js config file like so:

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        // specifying /(.*) in the source sets security headers on all routes
        source: "/(.*)",
        headers: [
          {
            key: "Content-Security-Policy",
            value:
              "default-src 'self'; font-src 'self' https://fonts.googleapis.com; img-src 'self' *.somewhere.com; script-src 'self'",
          },
          {
            key: "X-Frame-Options",
            value: "DENY",
          },
          {
            key: "X-Content-Type-Options",
            value: "nosniff",
          },
          {
            key: "Referrer-Policy",
            value: "origin-when-cross-origin",
          },
          {
            key: "Permissions-Policy",
            value: "camera=(); battery=(); geolocation=(); microphone=()",
          },
        ],
      },
    ];
  },
};

Each security headers has different options; you can only understand them by digging through MDN’s handy HTTP headers module.

I made several changes to the file, including the CSP and referrer policy. At first I left the referrer policy as is, but I received a warning from Security Headers and A as the grade. After digging around, I changed the Referrer-Policy to strict-origin-when-cross-origin, which is better because, according to web.dev’s referer and referrer-policy best practices, “it retains much of the referrer’s usefulness, while mitigating the risk of leaking data cross-origins.”

The score was now a perfect A+ but there was a gotcha.

Oops! A Bug

CSPs can be tough to setup. After deploying and getting my A+ badge, I tried shortening a URL and I couldn’t. Reading several stackoverflow threads made me realize the only source of problems was the CSP. I even tried to comment it out and the app worked fine.

A glance at the console and there were 100+ warnings and errors waiting for me to debug. It was time to try and understand what the options set in CSPs actually mean. Turns out you have to specify all domains that your website needs to call in the CSP. The alternative is to use a wildcard(*) which is nothing but secure. It allows an attacker to inject malicious code from any domain, the so-called Cross-site scripting (XSS) attack

In light of this, I had to define various domains, including my in-house API and those from analytics services. This fixed the problem.

The only issue at the moment is the inclusion of ‘unsafe-inline’ option which explains why the website scored grade A and not A+. There’s a reason for this, though.

The option acts as a fallback in older browsers when combined with strict-dynamic and https: and will be ignored if a hash or nonce is available. That means it’s still safe enough to prevent against “certain common XSS” attacks, according to Lukas Weichselbaum, a leader of Google’s web security team.

In the end, here’s how what my CSP contains various options:

`default-src 'self'; font-src 'self' https://fonts.googleapis.com data:; img-src 'self' https://*.google-analytics.com https://*.googletagmanager.com; script-src 'self' strict-dynamic 'unsafe-eval' https: 'unsafe-inline' unsafe-inline nonce-${nonce} https://*.googletagmanager.com; connect-src 'self' unsafe-eval https://*.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com https://vercel.live/* https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/css`;

Remember, there’s no one-size-fits-all solution for what to include in your CSP. It will vary depending on how strict you want your policy and also the precise values like domain names will depend on the services your app uses or talks to. Try different options, break things, fix things, iterate.

Next Steps

Obviously, this is not the strictest CSP by any standards, but I’m excited I was able to achieve this. I’ve learnt so much about security headers and I hope my knowledge only continues to grow in matters web security.