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
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
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 Medium
CVSS: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 Medium
CVSS: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_paramscallsget_and_store_nonce(self.authorization_url(), state) - lines 333 to 346:
get_and_store_noncestores the nonce as anOpenIdConnectAssociationkeyed byserver_urlandhandle=nonce. The state goes intoassoc_typebut is never read back. - lines 348 to 358:
get_noncelooks the nonce up byserver_urlandhandleonly - lines 393 to 415:
validate_claimsaccepts 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 Medium
CVSS: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 Medium
CVSS: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 Medium
CVSS: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 Medium
CVSS: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_paramscallsget_and_store_nonce(self.authorization_url(), state) - lines 333 to 346:
get_and_store_noncestores the nonce as anOpenIdConnectAssociationkeyed byserver_urlandhandle=nonce. The state goes intoassoc_typebut is never read back. - lines 348 to 358:
get_noncelooks the nonce up byserver_urlandhandleonly - lines 393 to 415:
validate_claimsaccepts 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 Medium
CVSS: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 Medium
CVSS: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
| Package | Affected | Fixed |
|---|---|---|
social-auth-corePyPI |
through 6.1.1 | 6.2.0 |
social-auth-coreDebian: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 less4.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-coreDebian: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 less4.9.1-15.0.2-15.1.0-15.1.0-25.1.1-1 |
None known |
social-auth-coreDebian: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 less4.9.1-15.0.2-15.1.0-15.1.0-25.1.1-1 |
None known |
social-auth-corePyPI |
before 6.2.0, plus listed versions 0.0.10.1.00.2.00.2.11.0.0 Show 45 moreShow less1.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-coreUbuntu:20.04:LTS |
All versions, plus listed versions 3.1.0-1 |
None known |
social-auth-coreUbuntu:22.04:LTS |
All versions, plus listed versions 3.1.0-1.14.1.0-14.2.0-1 |
None known |
social-auth-coreUbuntu:24.04:LTS |
All versions, plus listed versions 4.4.2-24.5.3-1 |
None known |
social-auth-coreUbuntu:26.10 |
All versions, plus listed versions 4.8.3-15.0.2-1 |
None known |
social-auth-coreUbuntu:26.04:LTS |
All versions, plus listed versions 4.6.1-14.8.1-14.8.3-1 |
None known |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE 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
github.com/python-social-auth/social-core/security/advisories/GHSA-h2r5-x5m7-3935
github.com/python-social-auth/social-core/pull/2141
github.com/python-social-auth/social-core/commit/c2f2ae4663bef19ee8b07f2cf5a2344f9155d826
github.com/python-social-auth/social-core/releases/tag/6.2.0
nvd.nist.gov/vuln/detail/CVE-2026-115910
github.com/advisories/GHSA-h2r5-x5m7-3935
Show 9 moreShow less
github.com/CVEProject/cvelistV5/tree/main/cves/2026/115xxx/CVE-2026-115910.json
security-tracker.debian.org/tracker/CVE-2026-115910
github.com/python-social-auth/social-core
ubuntu.com/security/CVE-2026-115910
cve.org/CVERecord?id=CVE-2026-115910
github.com/python-social-auth/social-core/security/advisories/ghsa-h2r5-x5m7-3935
security-tracker.debian.org/tracker/cve-2026-115910
github.com/cveproject/cvelistv5/tree/main/cves/2026/115xxx/cve-2026-115910.json
nvd.nist.gov/vuln/detail/cve-2026-115910
Left out here: 9 versions and the rest of 2 source descriptions. The JSON record has them.