Every CVE Search/ Get API key

CVE-2026-115910

Python Social Auth: OpenID Connect nonce is not bound to the browser session, so a stolen authorization code logs the attacker in as the victim

6.8 MediumFix available

Python Social Auth is a social authentication and registration mechanism. Prior to 6.2.0, OpenIdConnectAuth in social_core/backends/open_id_connect.py uses get_and_store_nonce to place the OpenID Connect nonce in a shared OpenIdConnectAssociation table, and get_nonce and validate_claims verify only that the nonce exists and has not expired rather than binding it to the browser session and OAuth state that initiated the login. An attacker who obtains a victim's unused authorization code can start a separate login to obtain valid state for the attacker's browser, submit the victim's code with that state, and pass nonce validation because the victim nonce is globally available. With DEFAULT_USE_PKCE disabled in the default OpenIdConnectAuth configuration, the attacker can be logged in as the victim. This issue is fixed in version 6.2.0.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
High
Privileges needed
None
User interaction
Required

What an attacker can affect

Confidentiality
High
Integrity
High
Availability
None

Exploit likelihood

0.16%Unchanged since Oct 27, 2026
90%50%10%1%

What each source reports

CVE List Score used 6.8

Python Social Auth: OpenID Connect nonce is not bound to the browser session, so a stolen authorization code logs the attacker in as the victim

Python Social Auth is a social authentication and registration mechanism. Prior to 6.2.0, OpenIdConnectAuth in social_core/backends/open_id_connect.py uses get_and_store_nonce to place the OpenID Connect nonce in a shared OpenIdConnectAssociation table, and get_nonce and validate_claims verify only that the nonce exists and has not expired rather than binding it to the browser session and OAuth state that initiated the login. An attacker who obtains a victim's unused authorization code can start a separate login to obtain valid state for the attacker's browser, submit the victim's code with that state, and pass nonce validation because the victim nonce is globally available. With DEFAULT_USE_PKCE disabled in the default OpenIdConnectAuth configuration, the attacker can be logged in as the victim. This issue is fixed in version 6.2.0.

Record
CVE-2026-115910
CVSS 3.1
6.8 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
Weakness
CWE-345, CWE-384
Updated
2026-11-02
EUVDEUVD-2026-109993 6.8

Python Social Auth is a social authentication and registration mechanism. Prior to 6.2.0, OpenIdConnectAuth in social_core/backends/open_id_connect.py uses get_and_store_nonce to place the OpenID Connect nonce in a shared OpenIdConnectAssociation table, and get_nonce and validate_claims verify only that the nonce exists and has not expired rather than binding it to the browser session and OAuth state that initiated the login. An attacker who obtains a victim's unused authorization code can start a separate login to obtain valid state for the attacker's browser, submit the victim's code with that state, and pass nonce validation because the victim nonce is globally available. With DEFAULT_USE_PKCE disabled in the default OpenIdConnectAuth configuration, the attacker can be logged in as the victim. This issue is fixed in version 6.2.0.

Record
EUVD-2026-109993
CVSS 3.1
6.8 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-h2r5-x5m7-3935 6.8

Python Social Auth: OpenID Connect nonce is not bound to the browser session, so a stolen authorization code logs the attacker in as the victim

Summary

The generic OpenID Connect backend (OpenIdConnectAuth and every backend built on it) stores the nonce in the shared Association table, not in the user's session. When the ID token comes back, validate_claims only checks that the nonce exists in that table and is not expired. It does not check that the nonce was issued to the browser that is finishing the login.

So the nonce does not do its job. OIDC Core (section 15.5.2) and RFC 9700 (section 4.5) use the nonce to stop authorization code injection: an attacker who gets hold of a victim's unused authorization code should not be able to redeem it in the attacker's own browser. With social-auth-core, the attacker can. They start their own login to get a valid state for their session, then hit the callback with the victim's code and their own state. state passes because it belongs to the attacker's session, and the nonce in the victim's ID token passes because it is in the global table. The attacker's browser is now logged in as the victim.

PKCE would block this, but OpenIdConnectAuth sets DEFAULT_USE_PKCE = False (the OAuth2 base class defaults to True), so the nonce is the only binding in the default setup.

Tested versions

social-auth-core 6.1.1 (latest), 6.1.0 and 5.2.0, with social-auth-app-django 7.1.0 and Django 6.1.2, Python 3.14. The same code is on master (098ef399c1).

Details

social_core/backends/open_id_connect.py in 6.1.1:

  • line 252: auth_params calls get_and_store_nonce(self.authorization_url(), state)
  • lines 333 to 346: get_and_store_nonce stores the nonce as an OpenIdConnectAssociation keyed by server_url and handle=nonce. The state goes into assoc_type but is never read back.
  • lines 348 to 358: get_nonce looks the nonce up by server_url and handle only
  • lines 393 to 415: validate_claims accepts any stored, unexpired nonce and removes it
  • line 110: DEFAULT_USE_PKCE = False

PoC

Everything runs locally. mockidp.py is a small OpenID provider on 127.0.0.1 that checks redirect_uri and enforces PKCE when a code_challenge was sent, like a normal provider. social_nonce.py uses two Django test clients as two browsers.

pip install social-auth-core social-auth-app-django django pyjwt cryptography
python social_nonce.py

Output on 6.1.1:

[inject] attacker session completes with victim code: (200, 'user=victim')

Controls:

MODE=control_victim python social_nonce.py
[control] victim completes normally: (200, 'user=victim')

MODE=control_unknown_nonce python social_nonce.py
[control] unknown nonce: ('EXC', 'AuthResponseError: The authentication response could not be matched to the request.')

PKCE=1 python social_nonce.py
[inject] attacker session completes with victim code: ('EXC', 'AuthCredentialError: The authorization code was rejected. Please restart login.')

So the nonce check runs, but it accepts a nonce that belongs to another browser. With PKCE on, the same attack fails.

mockidp.py:

"""Minimal local OpenID Provider on 127.0.0.1 for testing RP libraries."""
import json, time, secrets, threading, hashlib, base64, urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import jwt
from cryptography.hazmat.primitives.asymmetric import rsa

def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode()

class IdP:
    def __init__(self, port=0, client_id="rp-client", client_secret="rp-secret", name="idp"):
        self.key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
        self.kid = name + "-k1"
        self.client_id, self.client_secret = client_id, client_secret
        self.codes = {}
        self.access = {}
        self.claims_override = {}
        self.srv = ThreadingHTTPServer(("127.0.0.1", port), self._handler())
        self.port = self.srv.server_address[1]
        self.issuer = f"http://127.0.0.1:{self.port}"
        threading.Thread(target=self.srv.serve_forever,…
Record
GHSA-h2r5-x5m7-3935
CVSS 3.1
6.8 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
Weakness
CWE-345, CWE-384
Updated
2026-11-02
NVD 6.8

Python Social Auth is a social authentication and registration mechanism. Prior to 6.2.0, OpenIdConnectAuth in social_core/backends/open_id_connect.py uses get_and_store_nonce to place the OpenID Connect nonce in a shared OpenIdConnectAssociation table, and get_nonce and validate_claims verify only that the nonce exists and has not expired rather than binding it to the browser session and OAuth state that initiated the login. An attacker who obtains a victim's unused authorization code can start a separate login to obtain valid state for the attacker's browser, submit the victim's code with that state, and pass nonce validation because the victim nonce is globally available. With DEFAULT_USE_PKCE disabled in the default OpenIdConnectAuth configuration, the attacker can be logged in as the victim. This issue is fixed in version 6.2.0.

Record
CVE-2026-115910
CVSS 3.1
6.8 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
Weakness
CWE-345, CWE-384
Updated
2026-11-02
OSV 6.8

Python Social Auth: OpenID Connect nonce is not bound to the browser session, so a stolen authorization code logs the attacker in as the victim

Python Social Auth is a social authentication and registration mechanism. Prior to 6.2.0, OpenIdConnectAuth in social_core/backends/open_id_connect.py uses get_and_store_nonce to place the OpenID Connect nonce in a shared OpenIdConnectAssociation table, and get_nonce and validate_claims verify only that the nonce exists and has not expired rather than binding it to the browser session and OAuth state that initiated the login. An attacker who obtains a victim's unused authorization code can start a separate login to obtain valid state for the attacker's browser, submit the victim's code with that state, and pass nonce validation because the victim nonce is globally available. With DEFAULT_USE_PKCE disabled in the default OpenIdConnectAuth configuration, the attacker can be logged in as the victim. This issue is fixed in version 6.2.0.

Record
CVE-2026-115910
CVSS 3.1
6.8 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
Weakness
CWE-345, CWE-384
Updated
2026-11-02
OSVDEBIAN-CVE-2026-115910 6.8

Python Social Auth is a social authentication and registration mechanism. Prior to 6.2.0, OpenIdConnectAuth in social_core/backends/open_id_connect.py uses get_and_store_nonce to place the OpenID Connect nonce in a shared OpenIdConnectAssociation table, and get_nonce and validate_claims verify only that the nonce exists and has not expired rather than binding it to the browser session and OAuth state that initiated the login. An attacker who obtains a victim's unused authorization code can start a separate login to obtain valid state for the attacker's browser, submit the victim's code with that state, and pass nonce validation because the victim nonce is globally available. With DEFAULT_USE_PKCE disabled in the default OpenIdConnectAuth configuration, the attacker can be logged in as the victim. This issue is fixed in version 6.2.0.

Record
DEBIAN-CVE-2026-115910
CVSS 3.1
6.8 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
Updated
2026-11-02
OSVGHSA-h2r5-x5m7-3935 6.8

Python Social Auth: OpenID Connect nonce is not bound to the browser session, so a stolen authorization code logs the attacker in as the victim

Summary

The generic OpenID Connect backend (OpenIdConnectAuth and every backend built on it) stores the nonce in the shared Association table, not in the user's session. When the ID token comes back, validate_claims only checks that the nonce exists in that table and is not expired. It does not check that the nonce was issued to the browser that is finishing the login.

So the nonce does not do its job. OIDC Core (section 15.5.2) and RFC 9700 (section 4.5) use the nonce to stop authorization code injection: an attacker who gets hold of a victim's unused authorization code should not be able to redeem it in the attacker's own browser. With social-auth-core, the attacker can. They start their own login to get a valid state for their session, then hit the callback with the victim's code and their own state. state passes because it belongs to the attacker's session, and the nonce in the victim's ID token passes because it is in the global table. The attacker's browser is now logged in as the victim.

PKCE would block this, but OpenIdConnectAuth sets DEFAULT_USE_PKCE = False (the OAuth2 base class defaults to True), so the nonce is the only binding in the default setup.

Tested versions

social-auth-core 6.1.1 (latest), 6.1.0 and 5.2.0, with social-auth-app-django 7.1.0 and Django 6.1.2, Python 3.14. The same code is on master (098ef399c1).

Details

social_core/backends/open_id_connect.py in 6.1.1:

  • line 252: auth_params calls get_and_store_nonce(self.authorization_url(), state)
  • lines 333 to 346: get_and_store_nonce stores the nonce as an OpenIdConnectAssociation keyed by server_url and handle=nonce. The state goes into assoc_type but is never read back.
  • lines 348 to 358: get_nonce looks the nonce up by server_url and handle only
  • lines 393 to 415: validate_claims accepts any stored, unexpired nonce and removes it
  • line 110: DEFAULT_USE_PKCE = False

PoC

Everything runs locally. mockidp.py is a small OpenID provider on 127.0.0.1 that checks redirect_uri and enforces PKCE when a code_challenge was sent, like a normal provider. social_nonce.py uses two Django test clients as two browsers.

pip install social-auth-core social-auth-app-django django pyjwt cryptography
python social_nonce.py

Output on 6.1.1:

[inject] attacker session completes with victim code: (200, 'user=victim')

Controls:

MODE=control_victim python social_nonce.py
[control] victim completes normally: (200, 'user=victim')

MODE=control_unknown_nonce python social_nonce.py
[control] unknown nonce: ('EXC', 'AuthResponseError: The authentication response could not be matched to the request.')

PKCE=1 python social_nonce.py
[inject] attacker session completes with victim code: ('EXC', 'AuthCredentialError: The authorization code was rejected. Please restart login.')

So the nonce check runs, but it accepts a nonce that belongs to another browser. With PKCE on, the same attack fails.

mockidp.py:

"""Minimal local OpenID Provider on 127.0.0.1 for testing RP libraries."""
import json, time, secrets, threading, hashlib, base64, urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import jwt
from cryptography.hazmat.primitives.asymmetric import rsa

def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode()

class IdP:
    def __init__(self, port=0, client_id="rp-client", client_secret="rp-secret", name="idp"):
        self.key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
        self.kid = name + "-k1"
        self.client_id, self.client_secret = client_id, client_secret
        self.codes = {}
        self.access = {}
        self.claims_override = {}
        self.srv = ThreadingHTTPServer(("127.0.0.1", port), self._handler())
        self.port = self.srv.server_address[1]
        self.issuer = f"http://127.0.0.1:{self.port}"
        threading.Thread(target=self.srv.serve_forever,…
Record
GHSA-h2r5-x5m7-3935
CVSS 3.1
6.8 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
Weakness
CWE-345, CWE-384
Updated
2026-11-02
OSVUBUNTU-CVE-2026-115910 6.8

Python Social Auth is a social authentication and registration mechanism. Prior to 6.2.0, OpenIdConnectAuth in social_core/backends/open_id_connect.py uses get_and_store_nonce to place the OpenID Connect nonce in a shared OpenIdConnectAssociation table, and get_nonce and validate_claims verify only that the nonce exists and has not expired rather than binding it to the browser session and OAuth state that initiated the login. An attacker who obtains a victim's unused authorization code can start a separate login to obtain valid state for the attacker's browser, submit the victim's code with that state, and pass nonce validation because the victim nonce is globally available. With DEFAULT_USE_PKCE disabled in the default OpenIdConnectAuth configuration, the attacker can be logged in as the victim. This issue is fixed in version 6.2.0.

Record
UBUNTU-CVE-2026-115910
CVSS 3.1
6.8 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
social-auth-core
PyPI
through 6.1.1
6.2.0
social-auth-core
Debian:12
All versions, plus listed versions
4.3.0-14.4.2-14.4.2-24.5.3-14.5.4-1
Show 11 moreShow less
4.5.6-14.6.1-14.8.1-14.8.3-14.8.5-14.8.7-14.9.1-15.0.2-15.1.0-15.1.0-25.1.1-1
None known
social-auth-core
Debian:13
All versions, plus listed versions
4.6.1-14.8.1-14.8.3-14.8.5-14.8.7-1
Show 5 moreShow less
4.9.1-15.0.2-15.1.0-15.1.0-25.1.1-1
None known
social-auth-core
Debian:14
All versions, plus listed versions
4.6.1-14.8.1-14.8.3-14.8.5-14.8.7-1
Show 5 moreShow less
4.9.1-15.0.2-15.1.0-15.1.0-25.1.1-1
None known
social-auth-core
PyPI
before 6.2.0, plus listed versions
0.0.10.1.00.2.00.2.11.0.0
Show 45 moreShow less
1.0.11.1.01.2.01.3.01.4.01.5.01.6.01.7.02.0.03.0.03.1.03.2.03.3.03.3.13.3.23.3.33.4.04.0.24.0.34.1.04.2.04.3.04.4.04.4.14.4.24.5.04.5.14.5.24.5.34.5.44.5.54.5.64.6.04.6.14.7.04.8.04.8.14.8.24.8.34.8.44.8.54.8.64.8.74.9.04.9.1
6.2.0
social-auth-core
Ubuntu:20.04:LTS
All versions, plus listed versions
3.1.0-1
None known
social-auth-core
Ubuntu:22.04:LTS
All versions, plus listed versions
3.1.0-1.14.1.0-14.2.0-1
None known
social-auth-core
Ubuntu:24.04:LTS
All versions, plus listed versions
4.4.2-24.5.3-1
None known
social-auth-core
Ubuntu:26.10
All versions, plus listed versions
4.8.3-15.0.2-1
None known
social-auth-core
Ubuntu:26.04:LTS
All versions, plus listed versions
4.6.1-14.8.1-14.8.3-1
None known

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-115910 is a medium vulnerability (CVSS 6.8) in Python-social-auth Social-core, published October 26, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.16%, the 4th percentile. Fixed in social-auth-core 6.2.0.

References

Show 9 moreShow less
This page doesn't list everything

Left out here: 9 versions and the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open