Skip to content

OAuth PKCE code_verifier reused as state parameter — verifier leaked via authorization URL #10693

@shaun0927

Description

@shaun0927

Summary

The Anthropic OAuth PKCE flow in agent/anthropic_adapter.py reuses the PKCE code_verifier as the OAuth state parameter. Per RFC 6749 §10.12 and RFC 7636, these serve fundamentally different purposes and should be independent cryptographic values.

Affected Code

agent/anthropic_adapter.py:665-675:

verifier, challenge = _generate_pkce()

# ...
params = {
    "response_type": "code",
    "code_challenge": challenge,
    "code_challenge_method": "S256",
    "state": verifier,          # <-- verifier reused as state
    # ...
}

And on the callback path (anthropic_adapter.py:717-723):

"state": state,
"code_verifier": verifier,     # same value sent as both

Why this matters

The state parameter appears in the authorization URL — visible in:

  • Browser address bar and history
  • HTTP Referer headers on any navigation away from the authorization page
  • Authorization server access logs
  • Browser extensions with URL access

The code_verifier is meant to remain secret — known only to the client and used in the token exchange to prove the same client that initiated the flow is completing it. By reusing it as state, the verifier's secrecy is compromised.

Additionally, the state value is never validated against the original on callback. At line 712, the callback parses state from user input but does not compare it to the verifier that was originally generated. This means the CSRF protection that state is supposed to provide (RFC 6749 ��10.12) is absent.

Impact

  • Verifier leakage: An attacker who obtains the authorization URL (browser history, logs, referrer) learns the PKCE verifier and can complete the token exchange
  • No CSRF protection: Without state validation on callback, an attacker can craft a malicious authorization response and have it accepted

Suggested fix

Generate a separate cryptographic random value for state and validate it on callback:

import secrets

verifier, challenge = _generate_pkce()
state = secrets.token_urlsafe(32)  # independent value

params = {
    "code_challenge": challenge,
    "state": state,             # separate from verifier
    # ...
}

# On callback:
if received_state != expected_state:
    raise ValueError("OAuth state mismatch — possible CSRF")

Note

This is distinct from the broader OAuth client_id design choice discussed elsewhere. The PKCE state/verifier conflation is a spec-level implementation issue independent of how the OAuth client identifies itself.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High — major feature broken, no workaroundarea/authAuthentication, OAuth, credential poolscomp/agentCore agent loop, run_agent.py, prompt buildertype/securitySecurity vulnerability or hardening

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions