ProtonMail / ProtonMail/WebClients

[Security] SSO token + UID posted to window.opener without targetOrigin (session takeover)

Open
#522 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
5.6k
Forks
715
PR merge metrics
No merged PRs in 30d

Description

Summary

ExternalSSOConsumer in the account app forwards the SSO token + UID to window.opener via postMessage without a targetOrigin argument (defaults to "*") and without verifying the opener is a Proton origin. Any page that opens the SSO popup becomes the opener and receives the victim's session credentials.

Vulnerable code

applications/account/src/app/content/ExternalSSOConsumer.tsx:47-51 (current master):

if (uid && token && window.opener) {
    flow = ExternalSSOFlow.Sp;
    window.opener.postMessage({ action: 'sso', payload: { token, uid } });
    await wait(5000);
}

No second argument to postMessagetargetOrigin defaults to "*" → the message is delivered to any origin. The comment above the block states the intent ("opener means it was opened through another tab") but nothing verifies that the opener is actually a Proton tab.

Attack chain

  1. Victim (logged into Proton, active session) visits attacker's page.
  2. Attacker's page calls window.open('https://account.proton.me/sso/login?username=<victim>') — attacker's page becomes the opener.
  3. SSO flow completes with the victim's session; Proton redirects to /sso/login#token=<real>&uid=<real>.
  4. ExternalSSOConsumer parses token + uid from the hash and postMessagees them to the attacker's page.
  5. Attacker authenticates as the victim (session takeover).

Impact

Account/session takeover — attacker obtains a valid token/uid pair for the victim's account without the password.

Suggested fix

Always pass the expected origin (or the validated opener origin) as the second argument:

window.opener.postMessage({ action: 'sso', payload: { token, uid } }, 'https://account.proton.me');

Additionally, verify window.opener is a Proton origin before sending, or use a handshake (opener sends a challenge, popup replies).

Note

Reported publicly per the user's request; the code is already public in this repo. Happy to coordinate a private disclosure / remove this issue if Proton prefers handling it via security@proton.me first.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading applications/account/src/app/content/ExternalSSOConsumer.tsx around lines 47-51 and the surrounding SSO flow to understand how the opener is expected to be trusted. Done means SSO credentials are not sent to an arbitrary opener, using an agreed origin-validation or handshake approach, with the popup flow still working.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
authentication, frontend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.