Skip to content

Instantly share code, notes, and snippets.

@khalidx
Last active April 22, 2024 18:36
Show Gist options
  • Save khalidx/fc96bc28edeb1048d4cfeebb71a25d80 to your computer and use it in GitHub Desktop.
Save khalidx/fc96bc28edeb1048d4cfeebb71a25d80 to your computer and use it in GitHub Desktop.
Match nested page routes in Next.js middleware.

Description

Next.js doesn't seem to protect routes that use getServerSideProps().

How can you protect endpoints like:

/_next/data/*/*/something.json

Is this a bug? You can get 50% of the way there with middleware, but the middleware doesn't run on /_next/data/ unless you configure it to.

Make sure you audit your Next.js projects. There's a good official guide here:

https://nextjs.org/blog/security-nextjs-server-components-actions

But even that guide excludes the scenario I mentioned above.

So this is what's going on:

Next.js is normalizing /_next/data/build-id/hello.json to /hello for you. You can disable this if you want. But if you're protecting /hello, you should be good.

Unfortunately, static assets will still be served.

If you're protecting a secure part of your application like your admin dashboard, you probably don't want those pages or any of its assets served unless the user is signed in.

You can protect static assets too by matching on the following in your middleware.

To protect /admin and its subpages and assets, match on:

  • /admin/:path*
  • /_next/static/chunks/pages/admin:path*
  • /_next/static/chunks/pages/admin/:path*

You're welcome.

Please make sure to read the Note and Warning in the source code below before using this in your project.

Source

Copy this into your project. I saved mine in @/lib/matchNestedPages.ts.

import type { NextRequest } from 'next/server'

/**
 * To match on a prefix (like `/admin`) and its nested pages:
 * 
 * - `/admin`
 * - `/admin/*`
 * - `/_next/static/chunks/pages/admin*`
 * - `/_next/static/chunks/pages/admin/*`
 * 
 * Use the following function like so:
 * 
 * ```typescript
 * import { matchNestedPages } from '@/lib/matchNestedPages'
 * 
 * const admin = matchNestedPages({ prefix: '/admin' })
 * 
 * export const config = {
 *   matcher: [
 *     ...admin.matcher
 *   ]
 * }
 * 
 * export default async function middleware (request: NextRequest, context: NextFetchEvent) {
 *   if (admin.matches({ request })) {
 *     // do something here, like auth or rate limiting
 *   }
 * }
 * ```
 * 
 * Note:
 * 
 * - The prefix must not end in a slash and must not contain any matchers or `*` or regexes.
 *   Simply specify the route like `{ prefix: '/admin' }`. Not following these instructions
 *   will result in unexpected and possibly insecure and dangerous behavior.
 * - Ensure that paths don't clash in your application. Make sure there is only one `/admin` route.
 *   All paths and static assets starting with `/admin` will be matched by the `matches()` function.
 * 
 * Warning:
 * 
 * - If you have a route like `/admin` and another like `/administration`, this function will likely
 *   match both. Avoid naming routes like this with the same prefix in your application.
 */
export function matchNestedPages <Prefix extends `/${string}`> ({ prefix }: { prefix: Prefix }) {
  return {
    matcher: [
      // Like /admin and /admin/*
      `${prefix}/:path*`,
      // Like /_next/static/chunks/pages/admin*
      `/_next/static/chunks/pages${prefix}:path*`,
      // Like /_next/static/chunks/pages/admin/*
      `/_next/static/chunks/pages${prefix}/:path*`
    ] as const satisfies string[],
    matches ({ request }: { request: NextRequest }): boolean {
      if (request.nextUrl.pathname.startsWith(prefix)) return true
      if (request.nextUrl.pathname.startsWith(`/_next/static/chunks/pages${prefix}`)) return true
      return false
    }
  }
}

Usage

Here's how you could use this in your Next.js middleware.ts file:

import { matchNestedPages } from '@/lib/matchNestedPages'

const admin = matchNestedPages({ prefix: '/admin' })

export const config = {
  matcher: [
    ...admin.matcher
  ]
}

export default async function middleware (request: NextRequest, context: NextFetchEvent) {
  if (admin.matches({ request })) {
    // do something here, like auth or rate limiting
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment