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:
"Block all
%xxencodings 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 usesdecodeURI()(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."Double-slash bypass" is auto-mitigated on Next.js 15. The maintainer’s pathMatcher unit tests confirm
//api/admin/usersmatches the patchednormalizePath. But Next.js ≥ 15’s framework HTTP layer issuesHTTP/1.1 308withLocation: /api/admin/usersbefore 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.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
| File | Use |
|---|---|
sigma-http-request.yml | HTTP-layer probe — %xx-encoded unreserved characters in any path matched by your createRouteMatcher patterns |
sigma-application-log.yml | Application-side — log lines emitted by the patched middleware when it normalises a request and finds pathname !== rawPathname |
modsecurity.conf | Drop-in WAF recipe for fronting an unpatched Clerk app while you upgrade |
nextjs-mitigation-middleware.js | Stop-gap framework middleware for environments that can’t patch immediately — runs before Clerk and rejects unreserved-encoding probes against gated paths |
version-audit.sh | Crawls 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
| Surface | Verdict | Action |
|---|---|---|
@clerk/nextjs ≤ 6.39.1 + Next.js < 15 | PWNED, including double-slash bypass | Upgrade @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 ≥ 15 | PWNED via %xx. Double-slash auto-mitigated | Upgrade. Audit %xx only |
@clerk/astro or @clerk/nuxt (any version pulling shared ≤ 3.47.3) | PWNED, both vectors | Upgrade. Audit both %xx and // |
Custom Node integrations using @clerk/shared directly | PWNED, both vectors | Upgrade. Audit both vectors |
@clerk/shared ≥ 3.47.4 deployed | Patched | Confirm 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 ingithub.com/hexmortem/detection-rules. Confidential triage on this or related CVEs:scope@hexmortem.com.