Skip to content
← All posts

CVE Writeup

GHSA-vqx2 | Clerk SDK Exposes Every Middleware Protected Route

Technical analysis of GHSA-vqx2-fgx2-5wq9, a critical bypass (CVSS 9.1) in the Clerk route matcher for Next.js, Nuxt, Astro. How to spot it, how to mitigate, why middleware alone is not enough.

by SPECTROSEC Team 9 min Est. read
Web Security Auth Next.js SaaS

If you run a Next.js SaaS with Clerk in front, today is the day you bump package.json before standup. The advisory GHSA-vqx2-fgx2-5wq9 published on April 15, 2026 describes a middleware level auth gate bypass across all official Clerk SDKs for Next.js, Nuxt, Astro and shared. CVSS 9.1. No privileges required, no user interaction, network. The fix is a drop in replacement, but it needs an immediate bump plus a few extra checks inside route handlers.

During an assessment last week on an Italian fintech we flagged as medium an architecture that handled auth only inside middleware.ts. Yesterday that medium turned into a critical without a single line of code changing. Below is why, and how to check whether you are exposed.

Context

Clerk is the reference auth provider for the modern Next.js, Nuxt and Astro stack. The typical composition is this: a middleware at the edge of the app intercepts every request, decides whether the path requires an authenticated user and calls auth.protect() in that case. Everything else (API routes, server components, server actions) trusts the gate and moves on.

The advisory was opened on April 13, 2026 by Christiaan Swiers under responsible disclosure and closed on April 15 with patches published. No CVE number yet, but the GHSA identifier is already indexed by GitHub Security Database and by every SCA scanner.

Packages and versions

Package Vulnerable versions Patch
@clerk/nextjs 5.0.0 through 5.7.5, 6.0.0 through 6.39.1, 7.0.0 through 7.2.0 5.7.6, 6.39.2, 7.2.1
@clerk/nuxt 1.1.0 through 1.13.27, 2.0.0 through 2.2.1 1.13.28, 2.2.2
@clerk/astro 0.0.1 through 1.5.6, 2.0.0 through 2.17.9, 3.0.0 through 3.0.14 1.5.7, 2.17.10, 3.0.15
@clerk/shared 2.20.17 through 2.22.0, 3.0.0 through 3.47.3, 4.0.0 through 4.8.0 2.22.1, 3.47.4, 4.8.1

The range is huge because the affected function lives inside @clerk/shared, pulled in as a peer by every framework SDK. Bumping only @clerk/nextjs without also bumping @clerk/shared in the lockfile leaves the issue live.

Technical analysis

The broken function

At the heart of the gate is createRouteMatcher, a factory that builds a boolean function from a list of patterns:

import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isProtectedRoute = createRouteMatcher(['/admin(.*)', '/api/internal/(.*)']);

export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) {
    await auth.protect();
  }
});

The matcher compares the req path with the patterns. On match, auth.protect() validates the session. On miss, the request sails through without checks.

The mismatch

The bug is a classic interpretation conflict (CWE-436). Clerk normalizes the path one way. The underlying router (Next.js app router, Nuxt router, Astro integration) normalizes it another way. When the two normalizations diverge, an attacker can craft a request that:

  1. does not look protected to the Clerk matcher,
  2. arrives at the router as if it were the protected route.

In our internal SPECTROSEC tests we reproduced the pattern with three plausible vectors that fit the "interpretation conflict" profile:

  • Partial percent encoding: /%61dmin/dashboard | the matcher does not recognize /admin because it sees %61, the router decodes and resolves /admin/dashboard
  • Trailing slash plus query: /admin/./? | divergent normalization between regex matcher and router
  • Route group prefix: Next.js groups like (dashboard) that the matcher ignores but the router resolves

The official advisory does not publish the exact payload out of responsibility toward teams that have not yet upgraded. The concept is this: any path normalization that your matcher performs differently from your router is a potential bypass.

What stays protected

An important point, often underplayed in shallow readings. The advisory is explicit: session integrity is not compromised, impersonation is not possible, JWT tokens are validated correctly when they actually get checked. If inside the route handler you call:

// app/admin/dashboard/page.tsx
import { auth } from '@clerk/nextjs/server';

export default async function Page() {
  const { userId } = await auth();
  if (!userId) throw new Error('Unauthorized');
  // ...
}

that check still works. The problem is that most SaaS projects we see delegate auth exclusively to middleware because Clerk itself markets it as the elegant approach. Less code, less duplication, cleaner layout.

The moment middleware becomes bypassable, everything behind it becomes exposed.

Proof of concept

The skeleton to reproduce locally without touching production is simple:

# 1. Scaffold a Next.js project with Clerk
npx create-next-app@latest clerk-lab && cd clerk-lab
npm install @clerk/nextjs@6.39.1

# 2. Configure a route protected only by middleware
cat > middleware.ts <<'EOF'
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtected = createRouteMatcher(['/admin(.*)']);
export default clerkMiddleware(async (auth, req) => {
  if (isProtected(req)) await auth.protect();
});
EOF

# 3. Create /admin route without redundant auth()
mkdir -p app/admin
cat > app/admin/page.tsx <<'EOF'
export default function Admin() {
  return <pre>secret area</pre>;
}
EOF

# 4. Start and try path crafting vectors
npm run dev
curl -s http://localhost:3000/admin             # blocks (expected)
curl -s http://localhost:3000/%61dmin           # check whether it bypasses

The exact variance depends on the Next.js version and the Clerk hooks in use. Real pentest work is about systematically enumerating the router normalization patterns on the specific client, not building a universal script.

How to vet a production SaaS without active exploit

During a passive assessment it is enough to check three things in the client repo:

  1. Version pinning: cat package-lock.json | jq '.packages["node_modules/@clerk/shared"].version'. Anything inside the vulnerable range is a problem.
  2. Missing redundant auth: grep -rn "auth()" app/ src/ | grep -v middleware. If the project has heavy middleware and very few auth() calls inside routes, risk is high.
  3. Matcher shape: grep -rn "createRouteMatcher" .. Very broad patterns like ['(.*)'] or ['/api/(.*)'] widen the attack surface.

Starting this week that triad lives in every code review checklist we run on SaaS auth first projects.

Real world impact

The concrete scenarios you should picture depend on the product. Three examples from our recent client portfolio.

Italian B2B fintech. Middleware was protecting /api/internal/* which includes payout endpoints. Without the fix, an attacker with a crafted path could query merchant balances and possibly kick off a payout. With duplicate auth() in the handler the payout still needs validation, but the information disclosure on balances is already critical.

Corporate training e-learning platform. Middleware was guarding /admin/users/* where you can dump personal data of enrolled employees. GDPR Art. 32 breach if abused, Garante notification very plausible.

SaaS for law firms. Middleware gate on /admin/clients/*. The data in there are copies of judicial files. High CVSS confidentiality is an understatement.

The lesson, which we repeat to developers year after year, is that the authentication perimeter must be as close as possible to the data, not as far away as possible. The middleware is a convenient gate, not a sufficient defense.

Remediation

Step 1: version bump

# Next.js
npm install @clerk/nextjs@latest @clerk/shared@latest
# Nuxt
npm install @clerk/nuxt@latest @clerk/shared@latest
# Astro
npm install @clerk/astro@latest @clerk/shared@latest

# Verify that shared actually moved up
npm ls @clerk/shared

In monorepos with pnpm or yarn workspaces check your resolutions or overrides to force the version everywhere. We have seen projects where the @clerk/nextjs bump did not move @clerk/shared because it was pinned elsewhere.

Step 2: redundant auth in critical route handlers

Minimum policy to adopt right now:

// Every route handler that touches sensitive data
import { auth } from '@clerk/nextjs/server';

export async function GET(req: Request) {
  const { userId, has } = await auth();
  if (!userId) return new Response('Unauthorized', { status: 401 });
  if (!has({ role: 'admin' })) return new Response('Forbidden', { status: 403 });
  // business logic
}

You do not need to do this everywhere in minute zero. You need to do it right now on the three or four paths that hold the data you cannot afford to see leaked.

Step 3: monitoring

Add structured logs on protected routes recording userId, path, timestamp. A spike of 200 responses on admin paths with null userId is the signature of a bypass attempt in flight.

Step 4: matcher shape code review

Aggressive patterns like ['/(.*)'] with a whitelist to open public paths are safer than blacklists. If every path is protected by default and you only open what is public, a divergent normalization closes the route instead of opening it.

Notes from the SPECTROSEC field

Every time we code review a Next.js SaaS with Clerk the first check is this: how many auth() calls sit inside route handlers versus how many routes exist in total. If the ratio is below 30% we flag it as an architectural weakness even when no CVE is open. Bugs of this shape come back cyclically because the temptation to delegate to the edge is strong and auth provider marketing reinforces it.

The pattern we recommend to dev teams is a single wrapper like withAuth(handler, { role }) that every route must use. No route handler writes auth logic by hand. Nobody forgets the gate. Today's vulnerability becomes a version bump, not a redesign.

If you run a platform built on Clerk and want a quick check of the real exposure of your stack, write to info@spectrosec.com with subject "check clerk GHSA vqx2". We reply with a checklist and a time estimate within 48 hours.


Team SPECTROSEC | professional pentests, email info@spectrosec.com https://spectrosec.com