[HackerNotes Ep. 169] OAuth 2.1, MCP Authorization Security, and the Framework CVEs You Should Know

A practical look at OAuth 2.1 and MCP security pitfalls, from PKCE downgrades and SSRF tricks to token misuse and recent framework CVEs.

Hacker TL;DR

  • OAuth 2.1 mandates PKCE, so strip code_challenge from "2.1-compliant" servers to test for downgrade vulnerabilities

  • MCP's new Client Identity Metadata Documents (CIMD) turn the authorization server into an SSRF oracle via client_manifest_uri, logo_uri, and jwks_uri

  • Token passthrough in agentic workflows creates confused deputy vulnerabilities, so check if sub-agents inherit full-scope tokens

  • Framework CVEs are spiking as libraries adopt 2.1: fingerprint the OAuth stack and hunt implementation quirks

OAuth 2.1: What Changes and Why It Matters

The Core Protocol Shifts

OAuth 2.1 introduces four non-negotiable changes:

  1. PKCE is mandatory. Servers MUST reject authorization requests missing code_challenge. No exceptions, no optional flag.

  2. Implicit Flow is dead. Officially removed from the specification.

  3. ROPC is dead. Resource Owner Password Credentials flow removed entirely.

  4. Exact redirect URI matching. No more wildcard subdomains or loose path matching. Exact string comparison only.

  5. Bearer tokens prohibited in URIs. No more ?access_token= in query strings. Tokens go in the Authorization: Bearer header or, as a fallback, in a application/x-www-form-urlencoded POST body.

From a security testing perspective, the attack surface shifts significantly. The days of intercepting tokens from URL fragments (Implicit Flow) or harvesting access_token parameters from server logs and Referer headers are numbered, at least on compliant implementations.

The real vulnerabilities live in the transition gap. Targets claiming 2.1 compliance while maintaining backward compatibility with 2.0 clients are prime hunting ground.

We do subs at $25, $10, and $5, premium subscribers get access to:

Hackalongs: live bug bounty hacking on real programs, VODs available
Live data streams, exploits, tools, scripts & un-redacted bug reports

The PKCE Downgrade: Bug #1 to Test

In a standard PKCE flow, the client sends a code_challenge in the authorization request, the server stores it, and later the client proves possession by submitting the corresponding code_verifier during the token exchange. If they match, the token is issued.

The downgrade attack is straightforward: strip the code_challenge from the authorization request. If the server proceeds and issues an authorization code anyway, an attacker can intercept that code and exchange it without needing a verifier.

GET /authorize?response_type=code&client_id=app&redirect_uri=https://app.com/callback
# Notice: no code_challenge parameter

This is the test case for any target advertising 2.1 compliance. Many implementations added PKCE support without adding PKCE enforcement. The feature exists, but the server still happily processes requests without it.

Mobile apps are a prime target here. Developers often added PKCE for newer app versions while keeping the legacy non-PKCE endpoint alive for backward compatibility. That legacy endpoint is the interception vector.

MCP Authorization: The CIMD Threat Model

Brandyn dives into what happens when OAuth 2.1 intersects with the MCP (Model Context Protocol) authorization specification. This builds on top of RFC 8414 (server metadata discovery) and RFC 7591 (dynamic client registration) to create an entirely new threat model.

From Push to Pull: How Registration Changed

The traditional Dynamic Client Registration flow involves the client POSTing a manifest (client name, redirect URIs, logos, contacts) to a /register endpoint. The server creates a client_id and client_secret on the spot.

The new Client Identity Metadata Documents (CIMD) model reverses this. Instead of the client pushing data to the server, the authorization server pulls a client.json from the client's domain:

https://agent.example.com/.well-known/mcp/client.json

Trust is established via DNS/TLS: if the server can fetch the JSON document from the claimed domain, it trusts the contents.

Vector 1: SSRF on the Metadata Verification Service

This is the highest-signal attack class in the CIMD architecture. The authorization server (or its Metadata Verification Service) must make an outbound HTTP request to fetch the client_manifest_uri. This is basically a SSRF.

Test Case:

GET /authorize?client_manifest_uri=http://169.254.169.254/latest/meta-data/iam/security-credentials/

Sub-vectors to explore:

  • Protocol smuggling: client_manifest_uri=gopher://localhost:11211/X (Memcached manipulation)

  • Different redirect behavior: How does the MVS handle 301/302/307? Does it follow redirects to internal hosts?

  • User-Agent fingerprinting: Identify the HTTP library making the fetch to find library-specific quirks

  • JavaScript execution: Is the fetcher a headless browser?

Vector 2: Open Redirect via Validation Bypasses

The authorization server MUST enforce that redirect_uris inside the client.json belong to the same domain as the document. The vulnerability lives in how that domain matching is implemented.

{
  "client_id": "malicious_agent",
  "redirect_uris": [
    "https://evil-example.com/callback",
    "https://agent.example.com.evil.com/callback"
  ]
}

If the server performs a .contains() check or uses weak regex instead of strict domain parsing, it will accept agent.example.com.evil.com as belonging to agent.example.com, leaking the authorization code to the attacker's domain.

Vector 3: Secondary SSRF via Nested URIs

The client.json document contains additional URIs that the Resource Server or Tool Server may fetch independently:

{
  "client_id": "attack_agent",
  "logo_uri": "http://internal.admin.panel/api/v1/delete_user?id=123",
  "jwks_uri": "http://169.254.169.254/latest/meta-data/"
}

If the Tool Server blindly fetches logo_uri to render a "Trusted Agent" icon in the consent UI, it executes a secondary SSRF. The same applies to jwks_uri if the Resource Server fetches key material from an attacker-controlled or internal location.

Vector 4: TOCTOU via Caching

Since the CIMD is a static document, authorization servers may cache validated results aggressively. If an attacker can alter the client.json after initial validation (e.g., via a file upload vulnerability on the legitimate agent's domain), the server may still trust the cached copy while the live document now contains malicious redirect URIs.

Token Exchange (RFC 8693): Fixing the Delegation Mess

Authentication in 2026 is in a strong place. Delegation and authorization, however, remain a disaster, and this is where bugs proliferate in agentic workflows.

The Token Passthrough Problem

Right now, most multi-agent workflows pass the user's full-scope token downstream. Agent A receives the user token, spins up Agent B and Agent C, and simply forwards the same Authorization: Bearer [User_Token] header to each. Every agent in the chain inherits the full scope.

User Token (scope: full) → Agent A → Agent B → Agent C
                           (full)    (full)    (full)

RFC 8693 (Token Exchange) addresses this by enabling scope reduction at each delegation hop:

User Token (scope: full) → Agent A (scope: calendar.read)
                           → Agent B (scope: weather.read)
                           → Agent C (scope: files.list)

Each hop generates a new, narrowed token. The critical detail: the MCP specification currently lacks a mandate for token exchange, representing a significant security gap.

When testing an AI agent that invokes tools (Calendar, File Manager, Weather), intercept the traffic between the agent and the tool. If a Weather Tool receives a token with scope: mail.read, it can be a vulnerability. The tool can now pivot and read the user's email, even though the user only authorized a weather check.

Auth0's Token Vault already implements this architecture for AI agent integrations. Expect more providers to follow.

Framework CVEs: Where the Implementation Quirks Live

As frameworks and libraries race to implement 2.1 compliance, implementation quirks create exploitable gaps. Brandyn highlights several recent CVEs that demonstrate this pattern.

Django-allauth: Mutable Claims as Account UID

The vulnerability is elegant in its simplicity: Django-allauth used the preferred_username claim from the OAuth response as the account UID. The problem? preferred_username is mutable on providers like Okta and NetIQ. An attacker simply changes their preferred username to match the target user and achieves full account takeover.

Key findings from the audit:

  • Okta identifiers were mutable

  • NetIQ identifiers were mutable

  • Tokens for deactivated users could be refreshed indefinitely

  • Notion emails marked as verified without actual verification

Patched in version 65.13.0. Django-allauth remains the primary OAuth library for the Python ecosystem with over 2 million monthly downloads.

When reviewing any OAuth library, check which claim is used as the source of truth for user identity. If it's anything other than the sub (subject) claim, and especially if it's a user-writable field like preferred_username or email, test if overwriting that claim on the IdP side leads to impersonation.

CVE-2025-4144: Cloudflare Workers PKCE Bypass

The handleTokenRequest function in Cloudflare's Workers OAuth provider accepted a code_verifier even when the initial authorization request omitted the code_challenge. By stripping the challenge, an attacker could steal the authorization code and exchange it without a valid verifier.

This is a textbook PKCE downgrade affecting the entire Cloudflare Workers deployment base. The advisory explicitly notes that the MCP specification requires OAuth 2.0, making this directly relevant to agent-to-agent authentication flows.

CVE-2025-54576: OAuth2-Proxy Auth Bypass

Source: ZeroPath

OAuth2-Proxy is a widely deployed reverse proxy for Kubernetes and cloud-native environments. The skip_auth_routes feature allows operators to define regex patterns for paths that should bypass authentication.

The implementation flaw: the regex matched against the entire request URI, including query parameters.

skip_auth_routes = [ "^/foo/.*/bar$" ]

An attacker could access any protected endpoint by appending the bypass pattern as a query parameter:

GET /admin/secret?param=/foo/anything/bar HTTP/1.1

The regex matches against /admin/secret?param=/foo/anything/bar, finds the pattern in the query string, and skips authentication entirely.

JWT Algorithm Confusion: Still Here in 2026

The alg: none bypass is alive and well, with case variant tricks (nOnE, NoNE, NONE) still working against libraries that don't lowercase before checking. CVE-2026-23993 in HarbourJwt takes it further: any unrecognized algorithm value causes signature verification to be bypassed entirely. Set the algorithm to banana and the library fails open.

Actionable Takeaways

1. Hunt the Transition Gap. The biggest bugs are in compatibility shims between 2.0 and 2.1. When a target claims 2.1 compliance, you can try a bunch of bypass

2. CIMD is an SSRF Goldmine. The new pull-based client registration means the authorization server fetches attacker-influenced URLs by design. Test client_manifest_uri directly, then test logo_uri and jwks_uri inside the fetched document for secondary SSRF.

3. Follow the Token, Not the Login. Intercept agent-to-tool traffic. If the tool receives a token with broader scope than it needs, that is a confused deputy vulnerability. Document the lack of RFC 8693 token exchange.

4. Fingerprint the Framework. Identify which OAuth library the target uses, then review known CVEs for that specific version. Implementation quirks during spec transitions are where the bugs concentrate.

Resources

That's it for the week, keep hacking!