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):
- 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/CCVU#762428):JsonWebSignature.deserialize_json()initializedis_valid = Truebefore iterating overobj["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) andCVE-2026-114616(GHSA-8m4g-j4wp-wgcg):OctKey.import_key()checkedraw_key.startswith(POSSIBLE_UNSAFE_KEYS)only at byte0(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 anHS256HMAC secret, reopening theCVE-2024-37568algorithm-confusion attack. Separately,OctKeyaccepted an empty key (b""or"") as valid HMAC material (CVE-2026-114616, fixed in1.9.1).CVE-2026-116672(GHSA-c73q-w75p-j82v, CVSS 8.8 High, fixed in1.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 targetclient(token.check_client(client)), allowing any client with a valid registration token to overwrite another client’sredirect_urisor delete its registration.CVE-2026-114618(GHSA-fch4-3c6x-j4gc),CVE-2026-114620(GHSA-x4rv-c3wx-4cr8),CVE-2026-114619(GHSA-q686-2qmg-hqc7), andCVE-2026-114617(GHSA-c5mf-xmp2-59rc): DefaultJWTBearerGrant.get_audiences()returned[]so assertions for any audience were accepted; RFC 9101 JARrequest_uriwas fetched pre-authentication without checkingclient.request_uris(SSRF); OpenID implicit/hybrid grants checked rawrequest.payload.scopeinstead of filteredrequest.scope; and an ID token carrying"nonce_supported": falsedisabled its ownnoncevalidation.
- 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; andGHSA-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 themagicLink()plugin stored records in the sameverificationtable using unprefixed identifiers and loose JSON payloads containingemail. An unauthenticated attacker who knew a victim’s email address could start a social/OAuth login containing thatemailin the state payload and pass the returnedstateparameter to/magic-link/verifyas thetoken, signing in as the victim without ever reading their email.GHSA-r4xp-prcw-77qf(CVE-2026-110864, CVSS 7.4 High): Whenaccount.storeStateStrategy: "cookie"was used alongside theoAuthProxy()plugin without a separate proxy secret, both encrypted their JSON payloads under the same rootBETTER_AUTH_SECRETwithout domain separation. An attacker could initiate a social sign-in to obtain an encrypted state cookie containing a crafteduserprofile 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
| Ecosystem | Package | CVE / Advisory | Severity | Affected versions | Fixed version | Vulnerable component |
|---|---|---|---|---|---|---|
| PyPI | Authlib (authlib) | CVE-2026-96760 (GHSA-6v38-4h7w-rj9h) | CVSS 9.1 Critical | < 1.9.0 | 1.9.0 (1.9.1 current) | authlib/jose/rfc7515/jws.py (JsonWebSignature.deserialize_json) |
| PyPI | Authlib (authlib) | CVE-2026-116672 (GHSA-c73q-w75p-j82v) | CVSS 8.8 High | >= 0.15, < 1.9.1 | 1.9.1 | authlib/oauth2/rfc7592/endpoint.py (ClientConfigurationEndpoint.create_configuration_response) |
| PyPI | Authlib (authlib) | CVE-2026-114616 (GHSA-8m4g-j4wp-wgcg) | CVSS 7.4 High | < 1.9.1 | 1.9.1 | authlib/jose/rfc7518/oct_key.py (OctKey._check_key empty HMAC key) |
| PyPI | Authlib (authlib) | CVE-2026-114615 (GHSA-7vr2-2fgp-9w7p) | CVSS 7.4 High | >= 1.3.1, < 1.9.0 | 1.9.0 (1.9.1 current) | authlib/jose/rfc7518/oct_key.py (OctKey.import_key startswith bypass) |
| PyPI | Authlib (authlib) | CVE-2026-114618 (GHSA-fch4-3c6x-j4gc) | CVSS 6.8 Medium | < 1.9.0 | 1.9.0 | authlib/oauth2/rfc7523/jwt_bearer.py (JWTBearerGrant.verify_claims) |
| PyPI | Authlib (authlib) | CVE-2026-114620 (GHSA-x4rv-c3wx-4cr8) | CVSS 5.4 Medium | >= 1.6.0, < 1.9.0 | 1.9.0 | authlib/oauth2/rfc9101/authorization_server.py (JAR request_uri SSRF) |
| PyPI | Authlib (authlib) | CVE-2026-114619 (GHSA-q686-2qmg-hqc7), CVE-2026-114617 (GHSA-c5mf-xmp2-59rc) | Medium (CVSS 4.6 / 4.2) | < 1.9.0 | 1.9.0 | authlib/oidc/core/grants/* (request.payload.scope), base_client/*_openid.py (nonce_supported) |
| npm | better-auth | CVE-2026-114607 / CVE-2026-110862 (GHSA-965c-763c-88jm) | CVSS 9.1 Critical | >= 1.4.0-beta.18, < 1.7.7 | 1.7.7 | packages/better-auth/src/plugins/magic-link/index.ts, src/state.ts |
| npm | better-auth | CVE-2026-110864 (GHSA-r4xp-prcw-77qf) | CVSS 7.4 High | >= 1.5.0-beta.12, < 1.7.7 | 1.7.7 | packages/better-auth/src/plugins/oauth-proxy/index.ts, src/crypto/purpose.ts |
| npm | better-auth | CVE-2026-109346 (GHSA-q84f-53jg-9ppm) | CVSS 8.1 High | >= 1.3.8-beta.3, < 1.7.0-rc.3 | 1.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:
- Empty
signaturesarray inJsonWebSignature.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()inauthlib/jose/rfc7515/jws.pyinitializedis_valid = Truebefore looping overobj["signatures"]and only setis_valid = Falsewhen an element inside the loop failed verification. Passing"signatures": []skipped the loop body entirely and returned the unverifiedpayloadas valid without consulting the key. Commit6c11a02da8(v1.9.0) explicitly setsis_valid = Falsewhennot 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"]: - Prefixed PEM public keys and empty HMAC keys in
OctKey(CVE-2026-114615andCVE-2026-114616): To prevent RSA/EC public keys from being used as symmetricHS256HMAC secrets (CVE-2024-37568),OctKey.import_key()inauthlib/jose/rfc7518/oct_key.pycheckedif raw_key.startswith(POSSIBLE_UNSAFE_KEYS):. However,cryptography’sload_pem_public_key()andload_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()returnedFalseandOctKeyimported the public key as anHS256secret, allowing anyone with the public key to forge tokens. In addition,OctKey.import_key({"kty": "oct", "k": ""})bypassedimport_key()’s byte check altogether, andOctKeyaccepted empty bytes (b""/"") as a valid HMAC key. Commitse3bd41cb29(v1.9.0) and4ff30db442(v1.9.1) run_check_key()in bothimport_key()andget_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): Inauthlib/oauth2/rfc7592/endpoint.py,create_configuration_response()authenticated the bearer registration access token (token = self.authenticate_token(request)) and looked up the targetclient = self.authenticate_client(request), but never checked whethertokenwas issued toclient. Any dynamically registered client could present its own registration token while targeting another client’sclient_idto overwrite that victim client’sredirect_urisor delete it. Commit1bd2a31349(v1.9.1) enforcesif not token.check_client(client): raise AccessDeniedError(). - Mandatory
audcheck inJWTBearerGrant(CVE-2026-114618) and pre-auth SSRF in RFC 9101 JARrequest_uri(CVE-2026-114620): Inauthlib/oauth2/rfc7523/jwt_bearer.py,get_audiences()returned[]by default andverify_claims()only addedoptions["aud"]whenaudienceswas non-empty, allowing JWT assertions issued for another service to mint access tokens (e3bd41cb29now raisesInvalidGrantErrorifget_audiences()is empty). Inauthlib/oauth2/rfc9101/authorization_server.py, the pre-authbefore_get_authorization_granthook passed an unvalidatedrequest_uriquery parameter directly toget_request_object(request_uri)(6563f6f7a8enforces exact matching againstget_client_request_uris(client)before fetching). - OIDC scope escalation (
CVE-2026-114619) andnonce_supported=falseclaim bypass (CVE-2026-114617):OpenIDImplicitGrantandOpenIDHybridGrantpassed the rawself.request.payload.scopestring instead of the client-filteredself.request.scopetogenerate_token()(e3bd41cb29). Meanwhile,parse_id_token()insync_openid.pyandasync_openid.pycontainedif 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": falsein 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:
- Database-backed OAuth
statereused as a Magic Link token (CVE-2026-114607/CVE-2026-110862, CVSS 9.1 Critical): Whenaccount.storeStateStrategyis"database"(the default whenever a database is configured),generateGenericState()inpackages/better-auth/src/state.tswrites a row to theverificationtable withidentifier: state(or its SHA-256 hash whenverification.storeIdentifier: "hashed"). BecausestateDataSchemausedz.looseObject(...), a caller initiating a social or Generic OAuth login could include"email": "victim@example.com"in the state payload and receive the generatedstatestring in the OAuth redirect URL. When the caller then sentGET /api/auth/magic-link/verify?token=<state>, themagicLink()plugin looked upidentifier: storedTokenin the exact sameverificationtable, parsed{ email }fromvalue, and created an authenticated session forvictim@example.com. - Cookie-backed state replayed as an OAuth Proxy profile (
GHSA-r4xp-prcw-77qf/CVE-2026-110864, CVSS 7.4 High): Whenaccount.storeStateStrategy: "cookie"was configured,generateGenericState()encrypted the caller-influencedstateDataJSON withsymmetricEncrypt({ key: c.context.secretConfig, ... })and returned it to the browser in a cookie. If theoAuthProxy()plugin was enabled without a separate secret, the proxy callback decrypted incoming profile packages using the samec.context.secretConfig, allowing an attacker to pass a state-cookie ciphertext containing a forgeduserobject 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
- Upgrade
Authlibto1.9.1: Runpip install --upgrade "Authlib>=1.9.1" "joserfc>=1.6.8". Audit anyClientConfigurationEndpointsubclass to ensureTokenMixin.check_client()is implemented on your token model andclient_idis read from the endpoint URL path rather than the request body; ensureJWTBearerGrant.get_audiences()returns your token endpoint’s audience list; and implementget_client_request_uris()if you use RFC 9101JWTAuthenticationRequest. - Upgrade
better-authand all@better-auth/*packages to1.7.7: Runnpm install better-auth@^1.7.7(and update any@better-auth/drizzle-adapter,@better-auth/kysely-adapter, or@better-auth/oauth-providerpackages in lockstep). Deploy all server instances sharing a verification database or participating in anoAuthProxyflow together, and update any customverification.storeIdentifier.overridesrules to match themagic-link:andauth-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), and6563f6f7a8(CVE-2026-114617,CVE-2026-114620) forv1.9.0, followed by4ff30db442(CVE-2026-114616) and1bd2a31349(CVE-2026-116672) inv1.9.1. - 2026-10-25 to 2026-10-26: Better Auth merges PR
#11494(ac54bfd678) and shipsbetter-auth@1.7.7. - 2026-10-27 (
14:49to17:16 UTC): The GitHub Advisory Database, NVD, and CERT/CC (VU#762428) publish the reviewed advisories forAuthlib(CVE-2026-96760,CVE-2026-114615..114620,CVE-2026-116672) andbetter-auth(CVE-2026-114607/GHSA-965c-763c-88jm,GHSA-r4xp-prcw-77qf, andGHSA-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
- Authlib advisory GHSA-6v38-4h7w-rj9h and commit 6c11a02da8 (CVE-2026-96760: empty JWS JSON signatures)
- Authlib advisory GHSA-8m4g-j4wp-wgcg and commit 4ff30db442 (CVE-2026-114616: empty oct HMAC key)
- Authlib advisory GHSA-7vr2-2fgp-9w7p and commit e3bd41cb29 (CVE-2026-114615, CVE-2026-114618, CVE-2026-114619)
- Authlib advisory GHSA-c73q-w75p-j82v and commit 1bd2a31349 (CVE-2026-116672: RFC 7592 ClientConfigurationEndpoint)
- Authlib advisory GHSA-x4rv-c3wx-4cr8 and commit 6563f6f7a8 (CVE-2026-114620, CVE-2026-114617)
- Better Auth advisory GHSA-965c-763c-88jm (CVE-2026-114607: OAuth state accepted as Magic Link token)
- Better Auth advisory GHSA-r4xp-prcw-77qf and commit ac54bfd678 (PR #11494: isolate verification records and encryption purposes)
- CERT/CC Vulnerability Note VU#762428 (CVE-2026-96760)