VAITP Dataset

← Back to the dataset

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.

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_code

Patched 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 ::