Skip to main contentArrow Right

Table of Contents

Severity

Critical (CVSS 3.1: 9.1)

Affected package

@descope/nextjs-sdk (all versions through 0.15.55)

Fixed version

0.15.56

Advisory

https://github.com/descope/descope-js/security/advisories/GHSA-3j4q-2465-2m5r

CVE

Pending assignment

On July 17, 2026, we were notified of a critical authentication bypass vulnerability in @descope/nextjs-sdk. We shipped a fix within three hours of the report, notified all affected customers directly, and are publishing this writeup alongside the advisory, covering what the bug was, how we learned about it, what we did, and what we are changing.

We take the trust our customers place in us seriously, and a vulnerability in an authentication SDK is exactly the kind of thing that trust depends on. We are sorry this shipped, we are glad it was found and reported responsibly, and we have made the changes to drastically lower the odds of something like it happening again.

What happened

The session() and getSession(req) helper functions exported from @descope/nextjs-sdk/server are the standard way developers read the current user's session in a Next.js route handler or server component. Internally, these helpers checked for an x-descope-session HTTP header before falling back to validating the session cookie's JWT.

The problem was that when the header was present, its value was base64-decoded, parsed as JSON, and returned directly as the authenticated session object without signature validation.

In simplified form, the vulnerable code path looked like this:

// extractSession() in session.ts
const authInfo = JSON.parse(
  Buffer.from(descopeSession, 'base64').toString()
) as AuthenticationInfo;
return authInfo; // no validateJwt() call


// extractOrGetSession() in session.ts
const session = extractSession(sessionHeader);
if (session) {
  return session; // returned before the cookie path, which does call validateJwt()
}

The header check ran before the cookie-based JWT validation, and nothing stripped that header from inbound client requests. So any unauthenticated attacker could send a request like this:

PAYLOAD=$(echo "{jwt: 'not-a-jwt', token: {sub: 'victim-user-id', roles: ['admin']}}" | base64)
curl -H "x-descope-session: $PAYLOAD" https://victim-app.example.com/api/me

The application would then treat them as any user they specified, with any roles they specified, including admin.

Who was affected

Any Next.js application using session() or getSession(req) from @descope/nextjs-sdk/server for authentication or authorization decisions was affected, regardless of which version of the SDK they were running, since the flaw existed in every release through 0.15.55.

Root cause

Next.js splits request handling between middleware and downstream route handlers or server components, which run separately. To avoid re-validating the same JWT signature multiple times per request, the SDK's middleware would validate the session once against the cookie, then pass the already-validated result forward to downstream handlers by encoding it into the x-descope-session header. The downstream helpers trusted that header as pre-validated, since in the intended flow it was always set by the SDK's own middleware.

The flaw was that this header was never distinguished from one a client could set directly. Nothing checked whether x-descope-session originated from the middleware or from the request as received, so an attacker could simply supply the header themselves and skip the validated path. 

The fix, described below, removes the optimization that created this gap: every entry point now validates the JWT independently, at the cost of the redundant validation the original design was trying to avoid.

Our response and investigation

We reproduced the issue, confirmed the root cause, and published a patched version (0.15.56) within three hours of receiving the report. We then notified every identified affected customer directly, ahead of any public disclosure, with upgrade instructions and an interim mitigation for anyone who could not upgrade right away.

We held public disclosure until we could publish it alongside this advisory and the CVE assignment. This ensures that the technical details below, which are enough to build a working exploit, were not available to attackers before affected customers had a chance to patch.

Investigation details

Once we understood the mechanism, we checked our own infrastructure and worked with customers to check theirs. Based on that review, we found no evidence of active exploitation of this vulnerability prior to disclosure.

Descope NextJS SDK is open source, so the header name and the forgery mechanism were never secret. Another obstacle was that HTTP request headers are not typically captured by standard access logs, CDN logs, or APM tooling the way URLs, status codes, and query parameters are. In most environments, including ours, there was no log line that simply said "here are all requests that included an x-descope-session header," because nothing was ever configured to record that header's presence in the first place.

So instead of searching for the header directly, we looked for the kind of traffic pattern this attack would produce: single, one-off requests to authenticated Next.js routes with no normal session history, prior page views, or matching login events behind them. That is a distinctive shape for an attack that skips the login flow entirely, and we did not find it in the traffic we reviewed.

A review like this is not a guarantee, which is one of the reasons we prioritized getting a fix out and getting customers upgraded quickly, rather than treating this as lower urgency because we had not found exploitation yet.

For anyone facing a similar gap where the evidence you would want was never logged, here are a few approaches that generalize beyond this specific bug:

  • Cross-reference privileged actions against your own auth service's real login and token-issuance events. A forged session can perform admin actions, but it never generates a corresponding legitimate authentication event upstream. A privileged action with no matching login is a strong signal on its own.

  • Look for authenticated-looking requests that are missing the normal cookie footprint your app sets on real logins. An attacker using a header-based bypass has no reason to also carry a valid session cookie.

  • Check whether your load balancer, reverse proxy, or CDN can be configured to log specific custom headers going forward, even if it was not doing so at the time. AWS ALB, CloudFront, and most WAFs support selectively logging named headers, and turning this on for security-sensitive custom headers costs little and pays off the next time you need to investigate retroactively.

  • Treat single, isolated requests to sensitive endpoints (no browsing history, no prior session, straight to a privileged action) as worth flagging on their own, independent of any specific known exploit signature.

What we did

Updated the SDK

Version 0.15.56 does not add a missing validation check to the old design. It removes the trusted-header mechanism entirely. Two notable changes:

  • session() and getSession(req) no longer read x-descope-session at all. They now read a standard Authorization: Bearer <jwt> header (or fall back to the session cookie) and validate the JWT signature directly at the point of use every time.

  • The middleware now unconditionally strips any inbound x-descope-session header before forwarding the request downstream, whether the request is authenticated or not, so a client can no longer inject one regardless of outcome.

Notified affected customers directly 

We identified and emailed every customer using the affected SDK with upgrade instructions, an interim mitigation (stripping the descope session header at the edge, proxy, or WAF layer for anyone who could not upgrade immediately), and a way to confirm remediation back to us.

Coordinated with WAF vendors

We reached out to major WAF and CDN providers with a suggested detection rule (block or flag any inbound request carrying an x-descope-session header at the client-facing edge) so that customers had a mitigation layer available even before upgrading.

Credited the reporter

This vulnerability was responsibly disclosed to us by the EQSTLab research team through GitHub's private security advisory process. The clear writeup and working proof-of-concept they provided shortened our time to a confirmed fix.

What we are changing

  • Auditing every SDK we maintain for any code path that trusts client-supplied data (headers, cookies, or otherwise) without cryptographic validation.

  • Hardening our SDK release review process to flag any new code path that reads authentication state from a source other than a validated, signed token.

  • Updating our test suite conventions so that a test asserting that an unsigned input is accepted as valid is treated as a design red flag during review.

  • Continuing to work with CDN and WAF partners on faster, more standardized ways to ship emergency detection rules when a critical auth vulnerability is disclosed in a widely used SDK.

Timeline

  • The vulnerability was reported to us through our coordinated disclosure process on July 17, 2026 @ 13:13 UTC. 

  • Our security team validated the finding and released a patched version (0.15.56) by July 17, 2026 @ 16:33 UTC, within three hours of the report. 

  • From July 17 to July 23, 2026, we informed and worked with affected customers to upgrade their SDKs and implement interim mitigations if needed.

What you should do

If you have not already:

  1. Upgrade @descope/nextjs-sdk to 0.15.56 or later.

  2. If you cannot upgrade immediately, block or strip any inbound x-descope-session header at your edge, proxy, or WAF. This header should never legitimately originate from a client.

  3. If you have questions about whether your environment was affected, reach out to security@descope.com.