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
andWarning
in the source code below before using this in your project.
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
}
}
}
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
}
}