critical

CVE

CVE-2026-96760, CVE-2026-114607, CVE-2026-110862, CVE-2026-116672, CVE-2026-109346, CVE-2026-114616, CVE-2026-114615, CVE-2026-110864, CVE-2026-114618, CVE-2026-114620, CVE-2026-114619, CVE-2026-114617

CWE

CWE-347, CWE-327, CWE-287, CWE-862, CWE-918, CWE-345

Affected Surface

  • PyPI `Authlib` (`authlib`) versions `< 1.9.1` (`CVE-2026-114616`, `CVE-2026-116672`) and `< 1.9.0` (`CVE-2026-96760`, `CVE-2026-114615`, `CVE-2026-114617`, `CVE-2026-114618`, `CVE-2026-114619`, `CVE-2026-114620`)
  • npm `better-auth` versions `>= 1.4.0-beta.18, < 1.7.7` when using the `magicLink()` plugin with social or Generic OAuth and database-backed state (`CVE-2026-114607` / `CVE-2026-110862`)
  • npm `better-auth` versions `>= 1.5.0-beta.12, < 1.7.7` when using the `oAuthProxy()` plugin with cookie-backed state (`GHSA-r4xp-prcw-77qf` / `CVE-2026-110864`), and `>= 1.3.8-beta.3, < 1.7.0-rc.3` with `deviceAuthorization()` (`GHSA-q84f-53jg-9ppm` / `CVE-2026-109346`)

TL;DR

On 27 October 2026, 11 security advisories were published for two widely used authentication and OAuth/OIDC libraries: Python Authlib (1.9.0 and 1.9.1) and TypeScript better-auth (1.7.7 and 1.7.0-rc.3):

  1. Authlib < 1.9.1 (CVE-2026-96760, CVSS 9.1 Critical; CVE-2026-116672, CVSS 8.8 High; CVE-2026-114616, CVSS 7.4 High; CVE-2026-114615, CVSS 7.4 High; and four OAuth/OIDC protocol flaws):
    • CVE-2026-96760 (GHSA-6v38-4h7w-rj9h, CERT/CC VU#762428): JsonWebSignature.deserialize_json() initialized is_valid = True before iterating over obj["signatures"]. Supplying "signatures": [] skipped the loop and returned the attacker’s payload as verified without checking any signature or key.
    • CVE-2026-114615 (GHSA-7vr2-2fgp-9w7p) and CVE-2026-114616 (GHSA-8m4g-j4wp-wgcg): OctKey.import_key() checked raw_key.startswith(POSSIBLE_UNSAFE_KEYS) only at byte 0 (CVE-2026-114615). Any RSA/EC PEM public key or X.509 certificate preceded by a single newline, space, or comment bypassed the check and was accepted as an HS256 HMAC secret, reopening the CVE-2024-37568 algorithm-confusion attack. Separately, OctKey accepted an empty key (b"" or "") as valid HMAC material (CVE-2026-114616, fixed in 1.9.1).
    • CVE-2026-116672 (GHSA-c73q-w75p-j82v, CVSS 8.8 High, fixed in 1.9.1): In RFC 7592 Dynamic Client Configuration endpoints (ClientConfigurationEndpoint), create_configuration_response() never verified that the caller’s registration access token (token) belonged to the target client (token.check_client(client)), allowing any client with a valid registration token to overwrite another client’s redirect_uris or delete its registration.
    • CVE-2026-114618 (GHSA-fch4-3c6x-j4gc), CVE-2026-114620 (GHSA-x4rv-c3wx-4cr8), CVE-2026-114619 (GHSA-q686-2qmg-hqc7), and CVE-2026-114617 (GHSA-c5mf-xmp2-59rc): Default JWTBearerGrant.get_audiences() returned [] so assertions for any audience were accepted; RFC 9101 JAR request_uri was fetched pre-authentication without checking client.request_uris (SSRF); OpenID implicit/hybrid grants checked raw request.payload.scope instead of filtered request.scope; and an ID token carrying "nonce_supported": false disabled its own nonce validation.
  2. Better Auth < 1.7.7 (CVE-2026-114607 / CVE-2026-110862, CVSS 9.1 Critical; GHSA-r4xp-prcw-77qf / CVE-2026-110864, CVSS 7.4 High; and GHSA-q84f-53jg-9ppm / CVE-2026-109346, CVSS 8.1 High):
    • CVE-2026-114607 (GHSA-965c-763c-88jm, CVSS 9.1 Critical): Both database-backed OAuth sign-in state and the magicLink() plugin stored records in the same verification table using unprefixed identifiers and loose JSON payloads containing email. An unauthenticated attacker who knew a victim’s email address could start a social/OAuth login containing that email in the state payload and pass the returned state parameter to /magic-link/verify as the token, signing in as the victim without ever reading their email.
    • GHSA-r4xp-prcw-77qf (CVE-2026-110864, CVSS 7.4 High): When account.storeStateStrategy: "cookie" was used alongside the oAuthProxy() plugin without a separate proxy secret, both encrypted their JSON payloads under the same root BETTER_AUTH_SECRET without domain separation. An attacker could initiate a social sign-in to obtain an encrypted state cookie containing a crafted user profile and replay that ciphertext to the OAuth Proxy callback to sign in as any user.

What to do now: Upgrade Python Authlib to 1.9.1 (which also requires joserfc >= 1.6.8) and npm better-auth (plus any @better-auth/* adapters and plugins) to 1.7.7. Note that Authlib 1.9.0+ makes JWTBearerGrant.get_audiences() and RFC 9101 get_client_request_uris() mandatory, and better-auth 1.7.7 prefixes verification identifiers (magic-link: and auth-state:) and derives HKDF per-purpose encryption keys, so all Better Auth nodes sharing a database or OAuth Proxy flow must be upgraded together.

Affected packages and projects

EcosystemPackageCVE / AdvisorySeverityAffected versionsFixed versionVulnerable component
PyPIAuthlib (authlib)CVE-2026-96760 (GHSA-6v38-4h7w-rj9h)CVSS 9.1 Critical< 1.9.01.9.0 (1.9.1 current)authlib/jose/rfc7515/jws.py (JsonWebSignature.deserialize_json)
PyPIAuthlib (authlib)CVE-2026-116672 (GHSA-c73q-w75p-j82v)CVSS 8.8 High>= 0.15, < 1.9.11.9.1authlib/oauth2/rfc7592/endpoint.py (ClientConfigurationEndpoint.create_configuration_response)
PyPIAuthlib (authlib)CVE-2026-114616 (GHSA-8m4g-j4wp-wgcg)CVSS 7.4 High< 1.9.11.9.1authlib/jose/rfc7518/oct_key.py (OctKey._check_key empty HMAC key)
PyPIAuthlib (authlib)CVE-2026-114615 (GHSA-7vr2-2fgp-9w7p)CVSS 7.4 High>= 1.3.1, < 1.9.01.9.0 (1.9.1 current)authlib/jose/rfc7518/oct_key.py (OctKey.import_key startswith bypass)
PyPIAuthlib (authlib)CVE-2026-114618 (GHSA-fch4-3c6x-j4gc)CVSS 6.8 Medium< 1.9.01.9.0authlib/oauth2/rfc7523/jwt_bearer.py (JWTBearerGrant.verify_claims)
PyPIAuthlib (authlib)CVE-2026-114620 (GHSA-x4rv-c3wx-4cr8)CVSS 5.4 Medium>= 1.6.0, < 1.9.01.9.0authlib/oauth2/rfc9101/authorization_server.py (JAR request_uri SSRF)
PyPIAuthlib (authlib)CVE-2026-114619 (GHSA-q686-2qmg-hqc7), CVE-2026-114617 (GHSA-c5mf-xmp2-59rc)Medium (CVSS 4.6 / 4.2)< 1.9.01.9.0authlib/oidc/core/grants/* (request.payload.scope), base_client/*_openid.py (nonce_supported)
npmbetter-authCVE-2026-114607 / CVE-2026-110862 (GHSA-965c-763c-88jm)CVSS 9.1 Critical>= 1.4.0-beta.18, < 1.7.71.7.7packages/better-auth/src/plugins/magic-link/index.ts, src/state.ts
npmbetter-authCVE-2026-110864 (GHSA-r4xp-prcw-77qf)CVSS 7.4 High>= 1.5.0-beta.12, < 1.7.71.7.7packages/better-auth/src/plugins/oauth-proxy/index.ts, src/crypto/purpose.ts
npmbetter-authCVE-2026-109346 (GHSA-q84f-53jg-9ppm)CVSS 8.1 High>= 1.3.8-beta.3, < 1.7.0-rc.31.7.0-rc.3 (1.7.7 current)packages/better-auth/src/plugins/device-authorization/routes.ts

Technical analysis

1. Authlib JOSE signature bypasses: CVE-2026-96760, CVE-2026-114615, and CVE-2026-114616

Three of the Authlib advisories allow an unauthenticated attacker to forge JSON Web Signatures (JWS) or JSON Web Tokens (JWT) without knowing a valid signing key:

  1. Empty signatures array in JsonWebSignature.deserialize_json() (CVE-2026-96760, CVSS 9.1 Critical): When deserializing a general JWS JSON Serialization object (RFC 7515 Section 7.2.1), deserialize_json() in authlib/jose/rfc7515/jws.py initialized is_valid = True before looping over obj["signatures"] and only set is_valid = False when an element inside the loop failed verification. Passing "signatures": [] skipped the loop body entirely and returned the unverified payload as valid without consulting the key. Commit 6c11a02da8 (v1.9.0) explicitly sets is_valid = False when not obj["signatures"]:
    --- a/authlib/jose/rfc7515/jws.py
    +++ b/authlib/jose/rfc7515/jws.py
    @@ -204,6 +204,11 @@ def deserialize_json(self, obj, key, decode=None):
             headers = []
             is_valid = True
    +
    +        # invalidates cases like obj["signatures"] = None or obj["signatures"] = []
    +        if not obj["signatures"]:
    +            is_valid = False
    +
             for header_obj in obj["signatures"]:
  2. Prefixed PEM public keys and empty HMAC keys in OctKey (CVE-2026-114615 and CVE-2026-114616): To prevent RSA/EC public keys from being used as symmetric HS256 HMAC secrets (CVE-2024-37568), OctKey.import_key() in authlib/jose/rfc7518/oct_key.py checked if raw_key.startswith(POSSIBLE_UNSAFE_KEYS):. However, cryptography’s load_pem_public_key() and load_pem_x509_certificate() ignore leading whitespace, UTF-8 BOMs, or RFC 7468 preamble comments before -----BEGIN. If an application stored a PEM public key with even a single leading newline or space (b"\n-----BEGIN PUBLIC KEY-----..."), startswith() returned False and OctKey imported the public key as an HS256 secret, allowing anyone with the public key to forge tokens. In addition, OctKey.import_key({"kty": "oct", "k": ""}) bypassed import_key()’s byte check altogether, and OctKey accepted empty bytes (b"" / "") as a valid HMAC key. Commits e3bd41cb29 (v1.9.0) and 4ff30db442 (v1.9.1) run _check_key() in both import_key() and get_op_key():
    --- a/authlib/jose/rfc7518/oct_key.py
    +++ b/authlib/jose/rfc7518/oct_key.py
    @@ -65,6 +65,12 @@ def validate_raw_key(cls, key):
    +    @staticmethod
    +    def _check_key(raw_key):
    +        # Per RFC 7518 §3.2, an HMAC key must be at least as long as the hash output.
    +        if not raw_key:
    +            raise ValueError("oct key material must not be empty")
    +        # PEM parsers accept leading text, including comments and whitespace.
    +        if any(marker in raw_key for marker in POSSIBLE_UNSAFE_KEYS):
    +            raise ValueError("This key may not be safe to import")

2. Authlib OAuth2 / OIDC server & client flaws: CVE-2026-116672, CVE-2026-114618, CVE-2026-114620, CVE-2026-114619, and CVE-2026-114617

  • Cross-client registration takeover in RFC 7592 ClientConfigurationEndpoint (CVE-2026-116672, CVSS 8.8 High): In authlib/oauth2/rfc7592/endpoint.py, create_configuration_response() authenticated the bearer registration access token (token = self.authenticate_token(request)) and looked up the target client = self.authenticate_client(request), but never checked whether token was issued to client. Any dynamically registered client could present its own registration token while targeting another client’s client_id to overwrite that victim client’s redirect_uris or delete it. Commit 1bd2a31349 (v1.9.1) enforces if not token.check_client(client): raise AccessDeniedError().
  • Mandatory aud check in JWTBearerGrant (CVE-2026-114618) and pre-auth SSRF in RFC 9101 JAR request_uri (CVE-2026-114620): In authlib/oauth2/rfc7523/jwt_bearer.py, get_audiences() returned [] by default and verify_claims() only added options["aud"] when audiences was non-empty, allowing JWT assertions issued for another service to mint access tokens (e3bd41cb29 now raises InvalidGrantError if get_audiences() is empty). In authlib/oauth2/rfc9101/authorization_server.py, the pre-auth before_get_authorization_grant hook passed an unvalidated request_uri query parameter directly to get_request_object(request_uri) (6563f6f7a8 enforces exact matching against get_client_request_uris(client) before fetching).
  • OIDC scope escalation (CVE-2026-114619) and nonce_supported=false claim bypass (CVE-2026-114617): OpenIDImplicitGrant and OpenIDHybridGrant passed the raw self.request.payload.scope string instead of the client-filtered self.request.scope to generate_token() (e3bd41cb29). Meanwhile, parse_id_token() in sync_openid.py and async_openid.py contained if claims.get("nonce_supported") is False: claims.params["nonce"] = None, allowing an attacker’s ID token to disable its own replay protection simply by including "nonce_supported": false in the JWT payload (6563f6f7a8).

3. Better Auth 1.7.7: cross-flow state confusion (CVE-2026-114607 and GHSA-r4xp-prcw-77qf)

Both high-impact better-auth vulnerabilities fixed in commit ac54bfd678 (PR #11494, released in v1.7.7) stem from sharing a single storage namespace or symmetric encryption key across two different authentication flows without domain separation:

  1. Database-backed OAuth state reused as a Magic Link token (CVE-2026-114607 / CVE-2026-110862, CVSS 9.1 Critical): When account.storeStateStrategy is "database" (the default whenever a database is configured), generateGenericState() in packages/better-auth/src/state.ts writes a row to the verification table with identifier: state (or its SHA-256 hash when verification.storeIdentifier: "hashed"). Because stateDataSchema used z.looseObject(...), a caller initiating a social or Generic OAuth login could include "email": "victim@example.com" in the state payload and receive the generated state string in the OAuth redirect URL. When the caller then sent GET /api/auth/magic-link/verify?token=<state>, the magicLink() plugin looked up identifier: storedToken in the exact same verification table, parsed { email } from value, and created an authenticated session for victim@example.com.
  2. Cookie-backed state replayed as an OAuth Proxy profile (GHSA-r4xp-prcw-77qf / CVE-2026-110864, CVSS 7.4 High): When account.storeStateStrategy: "cookie" was configured, generateGenericState() encrypted the caller-influenced stateData JSON with symmetricEncrypt({ key: c.context.secretConfig, ... }) and returned it to the browser in a cookie. If the oAuthProxy() plugin was enabled without a separate secret, the proxy callback decrypted incoming profile packages using the same c.context.secretConfig, allowing an attacker to pass a state-cookie ciphertext containing a forged user object for any email address.

Commit ac54bfd678 separates both mechanisms:

--- a/packages/better-auth/src/plugins/magic-link/index.ts
+++ b/packages/better-auth/src/plugins/magic-link/index.ts
@@ -158,6 +158,26 @@
+const magicLinkRecordSchema = z
+	.object({
+		type: z.literal("magic-link"),
+		email: z.email(),
+		name: z.string().optional(),
+	})
+	.strict();
...
+const magicLinkIdentifier = (storedToken: string) =>
+	`magic-link:${storedToken}`;
@@ -243,8 +263,12 @@
 					await ctx.context.internalAdapter.createVerificationValue({
-						identifier: storedToken,
-						value: JSON.stringify({ email, name: ctx.body.name }),
+						identifier: magicLinkIdentifier(storedToken),
+						value: JSON.stringify({
+							type: "magic-link",
+							email,
+							name: ctx.body.name,
+						}),

For OAuth state and OAuth Proxy ciphertexts, packages/better-auth/src/crypto/purpose.ts now derives isolated 32-byte keys via HKDF-SHA256 (derivePurposeKey(secret, purpose)) with distinct info labels (better-auth:oauth-state-cookie:v1, better-auth:oauth-proxy-state:v1, better-auth:oauth-proxy-package:v1, and better-auth:oauth-proxy-profile:v1), while database-backed OAuth and SAML relay state records use the auth-state: identifier prefix.

Detection and remediation

# 1. Check Python and Node/TypeScript dependency manifests and lockfiles
rg -n -i '"?authlib"?\s*([~=!<>]=|:)' \
  requirements*.txt pyproject.toml uv.lock poetry.lock Pipfile.lock 2>/dev/null

rg -n '"(@better-auth/[a-z-]+|better-auth)"\s*:\s*"' \
  package.json package-lock.json pnpm-lock.yaml yarn.lock bun.lock 2>/dev/null

# 2. Search Python codebases for vulnerable Authlib call patterns
rg -n 'deserialize_json|ClientConfigurationEndpoint|JWTBearerGrant|JWTAuthenticationRequest|OpenIDImplicitGrant|OpenIDHybridGrant' \
  --glob '*.py' . 2>/dev/null

# 3. Search TypeScript/JavaScript codebases for Better Auth magicLink, oAuthProxy, or deviceAuthorization plugins
rg -n '\b(magicLink|oAuthProxy|deviceAuthorization)\s*\(' \
  --glob '*.{ts,tsx,js,jsx,mjs,cjs}' . 2>/dev/null
  1. Upgrade Authlib to 1.9.1: Run pip install --upgrade "Authlib>=1.9.1" "joserfc>=1.6.8". Audit any ClientConfigurationEndpoint subclass to ensure TokenMixin.check_client() is implemented on your token model and client_id is read from the endpoint URL path rather than the request body; ensure JWTBearerGrant.get_audiences() returns your token endpoint’s audience list; and implement get_client_request_uris() if you use RFC 9101 JWTAuthenticationRequest.
  2. Upgrade better-auth and all @better-auth/* packages to 1.7.7: Run npm install better-auth@^1.7.7 (and update any @better-auth/drizzle-adapter, @better-auth/kysely-adapter, or @better-auth/oauth-provider packages in lockstep). Deploy all server instances sharing a verification database or participating in an oAuthProxy flow together, and update any custom verification.storeIdentifier.overrides rules to match the magic-link: and auth-state: prefixes.

Timeline

  • 2026-10-14 to 2026-10-26: Authlib merges commits 6c11a02da8 (CVE-2026-96760), e3bd41cb29 (CVE-2026-114615, CVE-2026-114618, CVE-2026-114619), and 6563f6f7a8 (CVE-2026-114617, CVE-2026-114620) for v1.9.0, followed by 4ff30db442 (CVE-2026-114616) and 1bd2a31349 (CVE-2026-116672) in v1.9.1.
  • 2026-10-25 to 2026-10-26: Better Auth merges PR #11494 (ac54bfd678) and ships better-auth@1.7.7.
  • 2026-10-27 (14:49 to 17:16 UTC): The GitHub Advisory Database, NVD, and CERT/CC (VU#762428) publish the reviewed advisories for Authlib (CVE-2026-96760, CVE-2026-114615..114620, CVE-2026-116672) and better-auth (CVE-2026-114607 / GHSA-965c-763c-88jm, GHSA-r4xp-prcw-77qf, and GHSA-q84f-53jg-9ppm).

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References