Canva MCP — cross-origin OAuth account takeover

Operator origin:  |  Target: https://mcp.canva.com  |  Receiver: https://bb-canva-poc.pages.dev/r/  |  Header: X-Bug-Bounty: bugcrowdninja
Authorized testing under Canva's Bugcrowd program. Test account afrozenkiwi@gmail.com consents to itself.

1 · Register a public OAuth client cross-origin

redirect_uri:
no auth required, ACAO reflects this page's origin
(idle)

2 · Open Canva consent screen

→ 302 to www.canva.com — URL bar must show real canva.com
(idle)

3 · Captured codes (server-side receiver)

Endpoint: polling every 2s…
received code state ip / country action
no codes captured yet — open consent in step 2 and click Allow as the victim
Manual paste fallback (if the receiver is unreachable)
(idle)

4 · Exchange code for bearer (cross-origin, no client_secret, no code_verifier)

response body must be JS-readable (ACAO reflected)
(idle)

5 · List MCP tools (cross-origin Bearer)

expect 22 tools, ~46 KB body
(idle)

6 · Read real victim data (search-designs, read-only)

click "Render" on any row to load that design's thumbnail (presigned S3 URL — no auth needed)
Raw search-designs JSON
(idle)

7 · Persistence — refresh-token grant cross-origin

(idle)
Show variables
{}
Architecture of this PoC
This page is the OPERATOR CONSOLE. The full attack chain is split across
three origins — exactly how a real attacker would deploy it:

  1. attacker.example                       (this page, the operator console)
       - registers a public OAuth client at mcp.canva.com /register
       - opens the /authorize consent screen in a victim browser
       - polls bb-canva-poc.pages.dev/api/codes for incoming captures
       - on a hit, exchanges the code for a bearer (no client_secret, no PKCE)
         and reads victim designs via the MCP API

  2. claude.com/redirect/<URL>              (open redirect — pure proxy)
       - registered as redirect_uri at /authorize, so Canva trusts it
       - 302s the browser onward to the attacker's receiver, preserving ?code=

  3. bb-canva-poc.pages.dev/r/              (victim landing — Cloudflare Pages)
       - silently POSTs the OAuth code to /api/codes
       - location.replace()s the victim to https://www.canva.com so the
         flow ends on Canva and looks like a normal login

The victim never sees attacker.example. The browser address bar walks:
  attacker.example → www.canva.com → claude.com → bb-canva-poc.pages.dev → www.canva.com
and lands on Canva, while the operator console (this page) silently picks
up the code from the receiver and completes the takeover.
  
Notes on redirect_uri values that work
The Canva /authorize allowlist accepts these hosts (for any registered client):
  https://127.0.0.1:<port>/<path>       — needs a localhost listener
  https://localhost/<path>
  https://www.cursor.com/<path>          — code lands on a public domain (browser, history, extensions)
  https://chatgpt.com/<path>
  https://platform.openai.com/<path>
  https://claude.ai/<path>
  https://claude.com/<path>              — and this includes:
  https://claude.com/redirect/<FULL URL> — open redirect to ANY host. Use this to deliver
                                          the code to an attacker-controlled server with no
                                          victim cooperation.

This PoC chains claude.com/redirect → bb-canva-poc.pages.dev/r/ which silently
forwards the code to /api/codes and bounces the victim to www.canva.com.