hexmortem · 0x00400420confidentialv24.04 · sha 9f4e…a2c1pgp 0x783E 8C5A
LIVE · incident intake operational pgp 5421 993B … EAB8 0385 lat EU-SW · 42ms tz UTC+01 · AD
uptime 99.98% queue 3 active · 2 pending last note · 2026-05-30
CVE BRIEF

CVE-2026-41248 in Clerk: the percent-encoded character that walks past `auth()`

CVE-2026-41248

Verdict: exploitable

Affected

@clerk/nextjs ≤ 6.39.1, @clerk/nuxt ≤ 1.6.x, @clerk/astro ≤ 2.x — anything pulling @clerk/shared ≤ 3.47.3 transitively. Patch: clerk/javascript@b0b6675bad (PR #8311), 2026-04-15.

One-line. Clerk’s middleware tests the raw request pathname against a regex; the framework router decodes the path before dispatch. A single %xx-encoded character anywhere in the protected segment is sufficient to bypass createRouteMatcher. CVSS 9.1 / CWE-436. Patch in @clerk/shared 3.47.4 / @clerk/nextjs 6.39.2.

The attack chain (verified)

In a Next.js 15 app gated with createRouteMatcher(['/api/admin(.*)']):

GET /api/%61dmin/users HTTP/1.1

The middleware’s regex ^/api/admin.*$ tests against the literal string /api/%61dmin/users → no match → PASS. Next.js’s app-router then decodes the segment when matching /api/[...path], populates params.path = ['admin', 'users'], and dispatches the handler — unauthenticated.

We captured paramSegments: ["admin","users"] in the response: the smoking-gun proof of an interpretation conflict between middleware and router. Same shape works mid-segment (/api/a%64min/users, /api/adm%69n/users) — any single character anywhere in the protected segment.

What public summaries get wrong

Three common errors in downstream summaries:

  1. "Block all %xx encodings in admin paths" is wrong. A naive WAF rule false-positives on legitimate APIs that take encoded path components (e.g. /api/files/path%2Fto%2Fresource). The patch uses decodeURI() (which preserves %2F, %3F, %23) precisely so the matcher and the router agree on what counts as a path separator. Our published Sigma rule (sigma-http-request.yml) blocks only unreserved-character encodings via the regex %(?:[346][0-9a-fA-F]|[57][0-9a-eA-E]) — lifted from the maintainer’s own regression tests.

  2. "Double-slash bypass" is auto-mitigated on Next.js 15. The maintainer’s pathMatcher unit tests confirm //api/admin/users matches the patched normalizePath. But Next.js ≥ 15’s framework HTTP layer issues HTTP/1.1 308 with Location: /api/admin/users before middleware runs. We verified empirically. The Clerk patch’s slash-collapse logic is load-bearing only for non-Next.js consumers (Astro, Nuxt, custom Node integrations) and pre-15 Next.js. Triage by SDK + framework version, not by SDK version alone.

  3. The advisory text describes "crafted requests" without enumerating the bypass shapes. Defenders cannot deploy WAF rules from the advisory alone. We publish the canonical regex set lifted from packages/shared/src/__tests__/pathMatcher.spec.ts.

Detection — Sigma + ModSecurity + framework middleware

FileUse
sigma-http-request.ymlHTTP-layer probe — %xx-encoded unreserved characters in any path matched by your createRouteMatcher patterns
sigma-application-log.ymlApplication-side — log lines emitted by the patched middleware when it normalises a request and finds pathname !== rawPathname
modsecurity.confDrop-in WAF recipe for fronting an unpatched Clerk app while you upgrade
nextjs-mitigation-middleware.jsStop-gap framework middleware for environments that can’t patch immediately — runs before Clerk and rejects unreserved-encoding probes against gated paths
version-audit.shCrawls a list of package-lock.json paths and reports installed @clerk/shared versions

The application-log rule is the high-fidelity signal post-patch: it emits a structured event whenever the patched matcher rejects a request that the pre-patch matcher would have admitted, giving you a clean retro-attack window during incident response.

Triage decision tree

SurfaceVerdictAction
@clerk/nextjs ≤ 6.39.1 + Next.js < 15PWNED, including double-slash bypassUpgrade @clerk/shared to ≥ 3.47.4. Audit access logs for %xx and // in matcher-gated paths back ≥30 days
@clerk/nextjs ≤ 6.39.1 + Next.js ≥ 15PWNED via %xx. Double-slash auto-mitigatedUpgrade. Audit %xx only
@clerk/astro or @clerk/nuxt (any version pulling shared ≤ 3.47.3)PWNED, both vectorsUpgrade. Audit both %xx and //
Custom Node integrations using @clerk/shared directlyPWNED, both vectorsUpgrade. Audit both vectors
@clerk/shared ≥ 3.47.4 deployedPatchedConfirm with version-audit.sh

What this means for your stack

The general lesson: any auth or authorisation middleware that compares pre-decode request URLs against patterns that will be matched post-decode by a downstream router is a candidate for the same class of bug. Audit your stack for that pattern, not just for Clerk. Caddy, Nginx with Lua middleware, custom Express auth checks, and any framework-agnostic HTTP gateway are all in scope.

If you ship middleware that gates by URL: decodeURIComponent your inputs before regex-matching, and either (a) reject any path containing %2F/%3F/%23 so encoded-slash never matters, or (b) do a URL.pathname round-trip through the framework’s URL parser to get the canonical form your router will see.


Hexmortem Labs published this brief on 2026-04-26 as part of our public defender briefs. The reproducer lab, captured transcripts, full risk-assessment walkthrough, and detection bundle are at github.com/hexmortem/research-labs/tree/main/cve-2026-41248. The detections also live as standalone Sigma/YARA drops in github.com/hexmortem/detection-rules. Confidential triage on this or related CVEs: scope@hexmortem.com.