ProtonMail / ProtonMail/WebClients
[Security] SSO token + UID posted to window.opener without targetOrigin (session takeover)
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 postMessage → targetOrigin 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
- Victim (logged into Proton, active session) visits attacker's page.
- Attacker's page calls
window.open('https://account.proton.me/sso/login?username=<victim>')— attacker's page becomes theopener. - SSO flow completes with the victim's session; Proton redirects to
/sso/login#token=<real>&uid=<real>. ExternalSSOConsumerparsestoken+uidfrom the hash andpostMessagees them to the attacker's page.- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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