← WebKit Silent-Fix Report — 2026-W21

d85455322d  IndexedDB Connection/Transaction Identifier Confusion

severity high class CrossOrigin confidence 0.90 WebKit NetworkProcess IndexedDB exploitable-grade
Chris Dumez Sat May 23 23:32:02 2026 -0700 full: d85455322dae47dcb4235d0ca0dce0fee75a0fb5 bug report ↗ view on GitHub ↗
Primitive: unvalidated IDB connection/transaction identifiers across IPC = cross-origin data leak
Triage note: Adds IPC identifier validation at choke-points; a compromised web process could reference another's IDB connection/transaction.
Contents

The bug at a glance

This is a missing IPC authorization check in the NetworkProcess, a privileged process that services IndexedDB for all origins across all web content processes. Before the patch NetworkStorageManager looked up IndexedDB connection and transaction objects purely by attacker-supplied identifiers without verifying the object belonged to the IPC connection that sent the message, so a compromised or malicious WebContent process could reference another origin’s IDB connection/transaction and drive cross-origin data leakage or corruption. Severity is high because it crosses the origin/site boundary from a sandboxed content process; it is capped below critical because it requires an already-compromised or specially-crafted WebContent process (it is not reachable purely from unprivileged script) and the primitive is data disclosure/manipulation rather than direct memory corruption.

IndexedDB is implemented split-process: script in the WebContent process sends IPC to the NetworkProcess, which owns the real database connections and transactions and addresses them by numeric identifiers. Those identifiers are attacker-controllable message payloads, so a content process that forges or guesses another connection’s IDs can ask the NetworkProcess to operate on a victim origin’s database — the trust boundary is the IPC connection identity, which the old code never checked.

Root cause

The broken invariant is IPC ownership: every object the NetworkProcess exposes to a WebContent process must be reachable only by the IPC connection that created it. The identifiers carried in IDB messages (IDBDatabaseConnectionIdentifier, IDBResourceIdentifier and its embedded connectionIdentifier) are just integers in the message body and are fully controlled by the sender; they must therefore be resolved to objects only after confirming the resolved object is bound to the sending IPC::Connection.

Before the patch, IDBStorageRegistry::connection(IDBDatabaseConnectionIdentifier) simply did return m_connections.get(identifier); and transaction(IDBResourceIdentifier) did return m_transactions.get(identifier); after an isEmpty() guard. Neither took the IPC::Connection at all, so there was no way to check ownership — any content process that supplied a valid identifier belonging to another process got back the corresponding UniqueIDBDatabaseConnection or UniqueIDBDatabaseTransaction. The many NetworkStorageManager message handlers (establishTransaction, getRecord, getAllRecords, putOrAdd, openCursor, iterateCursor, createObjectStore, abortTransaction, commitTransaction, and so on) called these getters directly, so each was an unauthenticated cross-connection dereference. A few handlers had a partial MESSAGE_CHECK on transactionIdentifier.connectionIdentifier() existing, but that only checked the field was present, not that it belonged to the sender.

The fix threads the sending IPC::Connection& into every one of these handlers and into the registry getters, and centralizes the ownership check at the two choke-points. connection(identifier, ipcConnection) now looks up the UniqueIDBDatabaseConnection and then requires MESSAGE_CHECK_WITH_RETURN_VALUE(isValidConnectionForIPC(*databaseConnection, ipcConnection), …). transaction(identifier, ipcConnection) first MESSAGE_CHECKs that identifier.connectionIdentifier() is present, resolves the transaction, and then, if it has a databaseConnection(), runs the same isValidConnectionForIPC check. isValidConnectionForIPC maps the database connection back to its IDBConnectionToClient, finds the matching entry in m_connectionsToClient, and returns it->value->ipcConnection() == ipcConnection.uniqueID() — i.e., the object is only valid if its recorded owning IPC connection equals the connection the current message arrived on.

ensureConnectionToClient is likewise rebuilt to take IPC::Connection& and the full IDBResourceIdentifier, MESSAGE_CHECK the presence of requestIdentifier.connectionIdentifier(), and MESSAGE_CHECK that any pre-existing IDBStorageConnectionToClient’s ipcConnection() matches the sender’s uniqueID() — the old code merely ASSERTed this equality, which compiles out in release, so a mismatched connection was silently accepted in shipping builds. Because these getters now return nullptr (RefPtr) instead of a reference on a failed check, and MESSAGE_CHECK terminates the offending connection, forged identifiers are rejected at the choke-point and every downstream handler bails via its if (!transaction)/if (!connection) guard.

Key code

The new ownership choke-point: transaction() now requires the resolved object’s owning connection to match the sending IPC connection (IDBStorageRegistry.cpp).

bool IDBStorageRegistry::isValidConnectionForIPC(WebCore::IDBServer::UniqueIDBDatabaseConnection& databaseConnection, IPC::Connection& ipcConnection)
{
    auto connectionIdentifier = databaseConnection.connectionToClient().identifier();
    auto it = m_connectionsToClient.find(connectionIdentifier);
    if (it == m_connectionsToClient.end())
        return true;
    return it->value->ipcConnection() == ipcConnection.uniqueID();
}
// ...
    if (RefPtr databaseConnection = transaction->databaseConnection())
        MESSAGE_CHECK_WITH_RETURN_VALUE(isValidConnectionForIPC(*databaseConnection, ipcConnection), ipcConnection, nullptr);

Patch walkthrough

  • Source/WebKit/NetworkProcess/storage/IDBStorageRegistry.cpp — Adds the MESSAGE_CHECK_WITH_RETURN_VALUE macro and a new isValidConnectionForIPC() helper that maps a UniqueIDBDatabaseConnection back to its IDBConnectionToClient identifier, finds it in m_connectionsToClient, and confirms its recorded ipcConnection() equals the sending connection’s uniqueID(). connection() and transaction() are rewritten to take IPC::Connection&, resolve the object, then MESSAGE_CHECK isValidConnectionForIPC before returning it as a RefPtr (nullptr on failure). ensureConnectionToClient() now takes IPC::Connection& plus the IDBResourceIdentifier, MESSAGE_CHECKs the connectionIdentifier is present, and upgrades the old release-compiled-out ASSERT of ipcConnection equality into a hard MESSAGE_CHECK.
  • Source/WebKit/NetworkProcess/storage/IDBStorageRegistry.h — Updates the getter signatures to take IPC::Connection& and return RefPtr instead of raw pointer/reference, changes ensureConnectionToClient to take the IDBResourceIdentifier, and declares the private isValidConnectionForIPC() helper. This makes it structurally impossible to resolve an IDB object without presenting the sending connection.
  • Source/WebKit/NetworkProcess/storage/NetworkStorageManager.cpp — Threads the sending IPC::Connection& into every IDB message handler (establishTransaction, databaseConnectionPendingClose, abortOpenAndUpgradeNeeded, didFireVersionChangeEvent, didGenerateIndexKeyForRecord, didFinishHandlingVersionChangeTransaction, idbTransaction, get/getAll/getCount/deleteRecord, openCursor/iterateCursor, clearObjectStore, etc.) and passes it to the registry getters. openDatabase/deleteDatabase/getAllDatabaseNamesAndVersions switch from Ref to RefPtr with an early return when ensureConnectionToClient returns nullptr. Redundant per-handler MESSAGE_CHECKs on transactionIdentifier.connectionIdentifier() are removed since the check now lives at the choke-point.
  • Source/WebKit/NetworkProcess/storage/NetworkStorageManager.h — Updates the declarations of all those handlers and of idbTransaction() to carry the IPC::Connection& parameter, matching the .cpp so the connection identity flows to the choke-point on every IDB message.
  • Source/WebCore/Modules/indexeddb/server/IDBConnectionToClient.h — Marks IDBConnectionToClient::identifier() WEBCORE_EXPORT so the NetworkProcess-side registry (WebKit layer) can read the connection identifier when mapping a database connection back to its owning IPC connection in isValidConnectionForIPC.
  • Source/WebCore/Modules/indexeddb/server/UniqueIDBDatabaseTransaction.h — Marks UniqueIDBDatabaseTransaction::databaseConnection() WEBCORE_EXPORT so transaction() in the registry can reach the owning database connection to run the ownership check.

Background

NetworkProcess and split-process IndexedDB — WebKit runs IndexedDB in the privileged NetworkProcess rather than the sandboxed WebContent process. Script-side IDB calls are serialized into IPC messages that the NetworkProcess’s NetworkStorageManager handles; the actual UniqueIDBDatabase, UniqueIDBDatabaseConnection, and UniqueIDBDatabaseTransaction objects live only in the NetworkProcess. This isolation is a security boundary, so the NetworkProcess must treat everything a content process sends as untrusted.

MESSAGE_CHECK — A WebKit IPC hardening macro used in message handlers: if the asserted condition is false it treats the message as malicious, typically terminating the offending IPC connection (killing the misbehaving WebContent process) rather than continuing. MESSAGE_CHECK_WITH_RETURN_VALUE is the variant that also returns a value from the current function so a getter can bail out. These are the canonical way WebKit enforces that IPC payloads reference objects the sender is actually entitled to.

IDB identifiers as capabilities — IDBDatabaseConnectionIdentifier and IDBResourceIdentifier (which embeds a connectionIdentifier) are integer handles minted by the NetworkProcess and echoed back by the WebContent process in later messages. Because a compromised content process can put any integer in a message, these identifiers only function as security capabilities if the receiver validates that the referenced object belongs to the sending connection — otherwise they are guessable/forgeable pointers into another origin’s state.

IDBConnectionToClient / ipcConnection() — Each client IDB connection in the NetworkProcess is represented by an IDBStorageConnectionToClient that records the IPC::Connection::UniqueID of the WebContent process that created it via ipcConnection(). The registry keeps these in m_connectionsToClient keyed by IDBConnectionIdentifier. The new isValidConnectionForIPC compares this recorded owner against the sender of the current message to enforce ownership.

ASSERT vs MESSAGE_CHECK in release builds — The pre-patch ensureConnectionToClient used ASSERT(…ipcConnection() == connection), which is compiled out entirely in release/shipping builds. A security invariant guarded only by ASSERT is effectively unchecked in the field, which is why the fix promotes it to a MESSAGE_CHECK that fires in production and terminates the connection on mismatch.

RefPtr choke-point pattern — The fix converts the getters to return RefPtr (nullptr on failure) instead of a raw reference, so callers uniformly do ‘if (!connection) return;’. Concentrating the validation in connection()/transaction()/ensureConnectionToClient() means every current and future message handler that goes through these getters inherits the check, reducing the chance a new IPC entry point forgets to validate.

Vulnerability window

  1. Setup — A victim origin opens an IndexedDB database and starts a transaction; the NetworkProcess mints an IDBDatabaseConnectionIdentifier and transaction IDBResourceIdentifier bound to the victim’s IPC connection.
  2. Compromise — An attacker gains code execution in a separate WebContent process (or crafts malicious IPC), giving them the ability to send arbitrary IDB messages to the NetworkProcess.
  3. Forge identifier — The attacker sends, e.g., getAllRecords/getRecord/putOrAdd with the victim’s transaction identifier (or establishTransaction with the victim’s database-connection identifier).
  4. Unchecked resolve (bug) — Old transaction()/connection() do m_transactions.get / m_connections.get by identifier alone and hand back the victim’s object, ignoring which connection sent the message.
  5. Cross-origin action — The handler operates on the victim’s transaction/connection — reading records, mutating stores, aborting/committing — leaking or corrupting another origin’s IndexedDB data.
  6. Post-fix rejection — With the patch, isValidConnectionForIPC finds the object’s recorded owner does not equal the sender’s uniqueID(); MESSAGE_CHECK terminates the attacking connection and the getter returns nullptr.

Triggering

The patch adds no regression test (it is an IPC-layer hardening change). To trigger, an attacker needs a compromised WebContent process or a custom IPC client speaking the IDB message protocol to the NetworkProcess. One would open an IDB database/transaction from a victim origin to learn (or brute-force the small integer space of) its IDBDatabaseConnectionIdentifier / transaction IDBResourceIdentifier, then from a second, attacker-controlled connection send an IDB handler message (e.g. getAllRecords, putOrAdd, establishTransaction) carrying the victim’s identifier. On a pre-patch build the NetworkProcess resolves and operates on the victim’s object; on a patched build the MESSAGE_CHECK in connection()/transaction() kills the attacking connection.

Exploitation

  1. Foothold — Obtain arbitrary IPC-send capability from a sandboxed WebContent process (e.g. via a separate renderer RCE) — this bug is a boundary-crossing amplifier, not a first-stage primitive.
  2. Enumerate identifiers — Discover a target connection/transaction identifier: identifiers are small integers, and a colluding victim tab or brute force over the handle space can reveal a live one belonging to another origin.
  3. Cross-connection reference — Send an IDB message from the attacker connection carrying the victim’s identifier; pre-patch, the registry returns the victim’s UniqueIDBDatabaseTransaction/Connection with no ownership check.
  4. Data leak / tamper — Use getRecord/getAllRecords/openCursor to exfiltrate another origin’s stored data, or putOrAdd/deleteRecord/clearObjectStore to corrupt it; this is a confidentiality/integrity break across the origin boundary rather than memory corruption, and it stays that way.

Detection & hunting

For defenders and SOC / detection engineers:

  • NetworkProcess IPC-kill telemetry
  • Anomalous IDB cross-origin access
  • Version exposure

Audit directions

  • Narrow grep
  • Handler parameter audit
  • ASSERT-guarded invariants
  • Identifier-to-object choke-points

Before / after

Loading diff…