OOPUO
Review →
N.02
← Back to field notes
N.02 · Application · 16 AUG 2026

A router,
not a boundary.

Four Next.js advisories landed in 2026 for the same class of bug: a request that reaches the page under a shape the middleware matcher never learned. Then Clerk's own auth layer broke the identical way, twice.

Somebody forwarded a security advisory with a version number and "we should patch this" in the subject line. You bumped Next.js, watched the build go green, and closed the ticket. If the advisory was CVE-2026-64642 — App Router, Turbopack, one configured locale, published 21 Jul 2026 — you did close that specific door. What the ticket didn't ask is the question that actually matters: was the thing you were protecting ever behind the code you just patched, or was it only ever behind a route-matching regex that a differently shaped request could walk straight past?

The matcher's job

How a matcher becomes a gate.

A proxy.ts file — the renamed middleware.ts — exports a matcher array: something like export const config = { matcher: ['/dashboard/:path*', '/api/admin/:path*'] }. Read on its own, that array looks like a list of protected routes, and teams treat it that way — the matcher becomes the one place a project's authorization logic lives, sometimes the only place. What the array actually declares is narrower: which requests this file's code runs for. Next.js's own migration note renamed the file for exactly this reason — "proxy" is supposed to describe a boundary sitting in front of the app, the thing the old name let people assume it already was.

Four 2026 advisories are the same finding arriving through different doors. GHSA-267c-6grr-h53f reached a protected page through a client-side segment prefetch and the .rsc transport React Server Components use — neither URL shape matched what the matcher rule was written against. GHSA-492v-c6pp-mqqv let a crafted query parameter change which dynamic route value a page resolved, while the path string the matcher actually inspects stayed the same. GHSA-36qx-fr4f-26g5 found that Next.js quietly prepends a locale segment onto the matcher's own regex on the Pages Router, so an unprefixed request to /_next/data/<buildId>/<page>.json skips the file entirely and still returns the page's server-rendered data. Three of the four shipped in May's coordinated release of thirteen advisories; GHSA-6gpp-xcg3-4w24 (CVE-2026-64642), CVSS 8.3, followed in July on Turbopack App Router builds with one locale configured. A matcher is a routing convenience, not an authorization boundary — these four bugs are what happens when a team asks it to be one anyway.

Where authority lives

An authorization check needs somewhere authoritative to live.

Fixing the four advisories above closes four specific requests. The question underneath is where the authorization decision has to sit for it to hold regardless of which request finds it.

  1. 01

    Enforced where the data is read

    Next.js's own authentication guide describes Proxy as insufficient on its own, and says the majority of security checks belong as close as possible to the data source. A check that runs only in the router file guards one entrance; a request that reaches the same page by a different path never meets it.

  2. 02

    Deny by default, name the exceptions

    CVE-2026-41248 found that an allow-list matcher — a list of paths it protects — fails open on anything its regex doesn't happen to match. A deny-list matcher, one that protects everything and names the public routes as exceptions, fails closed on exactly the same class of miss.

  3. 03

    Authoritative no matter how it's called

    CVE-2026-42349 found that Clerk's auth.protect() silently drops any role, permission, feature or plan check the moment the same call also carries unauthenticatedUrl, unauthorizedUrl, or token. An authorization check that changes what it enforces based on which other options happened to be passed alongside it was never a single source of truth to begin with — it was several checks sharing one function name.

Three moves

Move the check to where the handler runs.

  1. Find every check that lives only in the router file

    Grep proxy.ts — or middleware.ts, if you haven't renamed it yet — for auth.protect(), a redirect, or a role check. For each hit, note what happens to the page it guards if a request reaches that page without this file running for it first.

  2. Re-run the same decision inside the handler

    Move the check into the function that actually reads the session, fetches the data, or renders the layout — the route handler, the server action, the page itself. The matcher can still run first and redirect the obvious cases early; the handler is where the decision has to be made again, on its own terms.

  3. Re-validate what the matcher already assumed

    A dynamic route value, a locale, a query parameter — if the matcher inferred any of them from the URL and the handler trusts that inference, re-check it inside the handler instead. The matcher's job ends at deciding to run the file; settling what the file does next belongs to the file.

Testing whether a request can reach a handler without passing your matcher means sending shaped requests — prefetch headers, .rsc paths, unprefixed locale segments, altered query parameters — at your own routes, or routes you've been asked in writing to look at. With your permission, OOPUO runs AI-assisted review against approved surfaces for exactly this class of check.

Application hardening

The matcher stays. The check moves.

The matcher decides where a request goes. Whether it's allowed to be there is a separate question — the one application hardening is built to answer, alongside the roles and data paths a fast-shipped handler exposes without meaning to.