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 the opener.
- SSO flow completes with the victim's session; Proton redirects to
/sso/login#token=<real>&uid=<real>.
ExternalSSOConsumer parses token + uid from the hash and postMessagees 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.
Summary
ExternalSSOConsumerin the account app forwards the SSO token + UID towindow.openerviapostMessagewithout atargetOriginargument (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):No second argument to
postMessage→targetOrigindefaults 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
window.open('https://account.proton.me/sso/login?username=<victim>')— attacker's page becomes theopener./sso/login#token=<real>&uid=<real>.ExternalSSOConsumerparsestoken+uidfrom the hash andpostMessagees them to the attacker's page.Impact
Account/session takeover — attacker obtains a valid
token/uidpair for the victim's account without the password.Suggested fix
Always pass the expected origin (or the validated opener origin) as the second argument:
Additionally, verify
window.openeris 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.