Skip to content
← Tutti gli articoli

CVE Writeup

GHSA-vqx2 | Clerk SDK Espone Ogni Route Protetta da Middleware

Analisi tecnica di GHSA-vqx2-fgx2-5wq9, bypass critico (CVSS 9.1) nel matcher di Clerk per Next.js, Nuxt, Astro. Come riconoscerlo, come mitigarlo, perché il middleware da solo non basta.

di Team SPECTROSEC 9 min Lettura stimata
Web Security Auth Next.js SaaS

Se hai un SaaS in Next.js con Clerk davanti, oggi è il giorno in cui aggiorni il package.json senza aspettare lo standup. L'advisory GHSA-vqx2-fgx2-5wq9 pubblicato il 15 aprile 2026 descrive un bypass del gate di auth a livello middleware in tutti gli SDK ufficiali Clerk per Next.js, Nuxt, Astro e shared. CVSS 9.1. Zero privilegi richiesti, zero interazione utente, rete. Il fix è drop in replacement, ma richiede un bump immediato e qualche controllo extra nei route handler.

Durante un assessment la scorsa settimana su una fintech italiana avevamo marcato come finding medio un'architettura che gestiva l'auth solo nel middleware.ts. Ieri quel medio è diventato un critico senza muovere una riga di codice. Racconto sotto perché, e come verificare se siete esposti.

Contesto

Clerk è il provider auth di riferimento per lo stack moderno Next.js, Nuxt e Astro. La composizione tipica è questa: un middleware al bordo dell'applicazione intercetta ogni richiesta, decide se il path richiede utente autenticato e chiama auth.protect() in quel caso. Tutto il resto (API routes, server components, server actions) si fida del gate e va avanti.

L'advisory è stato aperto il 13 aprile 2026 da Christiaan Swiers con responsible disclosure e chiuso il 15 con le patch pubblicate. Nessun CVE numerico al momento, ma l'identificativo GHSA è già censito da GitHub Security Database e da tutti gli scanner SCA.

Pacchetti e versioni

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

Il range è enorme perché la funzione incriminata è in @clerk/shared, trascinata come peer di tutti gli SDK framework. Aggiornare il solo @clerk/nextjs senza bumpare @clerk/shared nel lockfile lascia il problema attivo.

Analisi tecnica

La funzione incriminata

Il cuore del gate è createRouteMatcher, una factory che costruisce una funzione booleana a partire da un elenco di pattern:

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();
  }
});

Il matcher confronta il path della req con i pattern. Se matcha, auth.protect() verifica la sessione. Se non matcha, la richiesta passa senza controlli.

Il disallineamento

Il bug è una classica interpretation conflict (CWE-436). Clerk normalizza il path in un modo. Il router sottostante (Next.js app router, Nuxt router, Astro integration) lo normalizza in un altro. Quando le due normalizzazioni divergono l'attaccante può costruire una richiesta che:

  1. al matcher Clerk non sembra protetta,
  2. al router arriva come se fosse la route protetta.

Nei test interni SPECTROSEC abbiamo ricostruito il pattern con tre vettori plausibili che cadono dentro il profilo "interpretation conflict":

  • Percent encoding parziale: /%61dmin/dashboard | il matcher non riconosce /admin perché vede %61, il router decodifica e risolve /admin/dashboard
  • Slash terminante + query: /admin/./? | normalizzazione divergente tra regex matcher e router
  • Prefisso di route group: gruppi Next.js tipo (dashboard) che il matcher non tocca ma che il router risolve

L'advisory ufficiale non pubblica il payload esatto per responsabilità verso chi non ha ancora aggiornato. Il concetto è questo: qualunque normalizzazione del path che il vostro matcher fa in modo diverso dal vostro router è potenzialmente un bypass.

Cosa resta protetto

Un punto importante, spesso sottovalutato in letture superficiali. L'advisory è chiaro: la session integrity non è compromessa, l'impersonation non è possibile, i token JWT sono validati correttamente quando vengono effettivamente controllati. Se dentro il route handler chiamate:

// 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');
  // ...
}

quella verifica continua a funzionare. Il problema è che la maggior parte dei progetti SaaS che abbiamo visto delega l'auth esclusivamente al middleware perché Clerk stesso lo vende come il punto elegante. Meno codice, meno duplicazione, più pulizia.

Nel momento in cui il middleware può essere bypassato, tutto il codice dietro diventa esposto.

Proof of concept

Lo schema per replicare in locale senza toccare produzione è semplice:

# 1. Crea un progetto Next.js con Clerk
npx create-next-app@latest clerk-lab && cd clerk-lab
npm install @clerk/nextjs@6.39.1

# 2. Configura un route protetto solo da 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. Crea la route /admin senza auth() ridondante
mkdir -p app/admin
cat > app/admin/page.tsx <<'EOF'
export default function Admin() {
  return <pre>secret area</pre>;
}
EOF

# 4. Avvia e prova vettori di path crafting
npm run dev
curl -s http://localhost:3000/admin             # blocca (atteso)
curl -s http://localhost:3000/%61dmin           # verifica se bypassa

La varianza esatta dipende dalla versione di Next.js e dagli hook Clerk attivi. Il lavoro di pentest serio è enumerare sistematicamente i pattern normalization del router sul cliente specifico, non fare uno script universale.

Come verificare un SaaS in produzione senza exploit attivo

Durante un assessment passivo basta guardare tre cose nel repo del cliente:

  1. Pin delle versioni: cat package-lock.json | jq '.packages["node_modules/@clerk/shared"].version'. Qualunque cosa nel range vulnerabile è un problema.
  2. Assenza di auth ridondante: grep -rn "auth()" app/ src/ | grep -v middleware. Se il progetto ha molto middleware e pochissime chiamate auth() dentro le route, il rischio è alto.
  3. Shape del matcher: grep -rn "createRouteMatcher" .. Pattern molto ampi come ['(.*)'] o ['/api/(.*)'] amplificano la superficie.

Questa triade la infiliamo in ogni checklist di code review per progetti SaaS auth-first dal lunedì in poi.

Impatto reale

Gli scenari concreti che dovete immaginare dipendono dal prodotto. Tre esempi dal nostro portfolio di clienti recenti.

Fintech italiana B2B. Il middleware proteggeva /api/internal/* che include gli endpoint di payout. Senza fix, un attaccante con un path crafted può interrogare saldo dei merchant e forse iniziare un payout. Con auth() duplicato nell'handler il payout richiede comunque validazione, ma l'information disclosure sui saldi è critica.

Piattaforma e-learning per un'azienda di formazione corporate. Il middleware proteggeva /admin/users/* da cui si possono dumpare i dati anagrafici dei dipendenti corsisti. Violazione GDPR Art. 32 se sfruttata, segnalazione al Garante plausibile.

SaaS per studi legali. Middleware gate su /admin/clients/*. I dati lì dentro sono copie di fascicoli giudiziari. CVSS confidentiality alto è un eufemismo.

La lezione, che ripetiamo da anni agli sviluppatori, è che il perimetro di autenticazione deve essere il più vicino possibile al dato, non il più lontano. Il middleware è un gate comodo, non una difesa sufficiente.

Remediation

Passo 1: bump delle versioni

# 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

# Verifica che shared sia effettivamente salito
npm ls @clerk/shared

In monorepo con workspace pnpm o yarn controllate i resolutions o overrides per forzare la versione dovunque. Abbiamo visto progetti in cui il bump di @clerk/nextjs non ha mosso @clerk/shared perché pinnato altrove.

Passo 2: auth ridondante nei route handler critici

Policy minima da adottare subito:

// Tutte le route handler che toccano dati sensibili
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
}

Non serve farlo ovunque al minuto zero. Serve farlo subito nei tre o quattro path che custodiscono i dati che perderebbero il cliente se diventassero pubblici.

Passo 3: monitoring

Aggiungete un log strutturato nelle route protette che registra userId, path, timestamp. Un picco di richieste 200 su path admin con userId nullo è il segnale di un tentativo di bypass in corso.

Passo 4: code review della shape del matcher

Pattern aggressivi come ['/(.*)'] con white list per aprire i path pubblici sono più sicuri della black list. Se ogni path è protetto by default e aprite solo ciò che è pubblico, una normalizzazione divergente chiude la route invece di aprirla.

Note dal campo SPECTROSEC

Ogni volta che facciamo code review su un SaaS Next.js con Clerk il primo controllo è: quante chiamate auth() ci sono dentro i route handler rispetto a quante route ci sono in totale. Se il rapporto è sotto il 30% segnaliamo la cosa come architectural weakness anche quando non c'è una CVE aperta. I bug di questo tipo vengono fuori ciclicamente perché la tentazione di delegare al bordo è forte e il marketing dei provider auth la rinforza.

Il pattern che suggeriamo ai team dev è un wrapper unico tipo withAuth(handler, { role }) che ogni route deve usare. Nessun route handler scrive auth logic a mano. Nessuno dimentica il gate. La vulnerabilità di oggi diventa un bump di versione, non un redesign.

Se siete amministratori di una piattaforma basata su Clerk e volete un check rapido dell'esposizione reale del vostro stack, scrivete a info@spectrosec.com con oggetto "check clerk GHSA vqx2". Vi rispondiamo con una checklist e una stima di tempo nelle 48 ore successive.


Team SPECTROSEC | pentest professionali, scrivimi a info@spectrosec.com https://spectrosec.com