[5] IndexedDB Connection/Transaction Identifier Confusion
IndexedDB handed out other processes' transactions to whoever named them.
Medium. The bug hands a compromised renderer another process's IndexedDB transactions — read, write, and destroy — bypassing per-connection site restrictions entirely. It stays out of the High band because it is strictly post-compromise and the disclosure half depends on guessing identifiers whose predictability the change does not establish.
Cross-process authorization on a shared broker turns on one question: does possession of a name amount to possession of the thing named? WebKit's IndexedDB data lives in the network process, which serves IDB messages from every WebContent process in a session, so its registry of identifier-to-object mappings is shared across mutually distrusting clients. The threat model treats a WebContent process as potentially compromised, meaning every field of every message it sends is attacker-chosen. The invariant that should follow is that an identifier carried in an IPC message may only resolve to an object owned by the sending connection.
The angle: a WebContent process already under attacker control can name another process's IDB transaction and drive reads, writes, aborts and object-store deletion against origins it was never permitted to open.
From the commit message:
NetworkStorageManagerfails to validate that Connection/Transaction identifiers belong to the IPC connection that sent the IPC. This could lead to data leakage.I added the
MESSAGE_CHECKcalls inside theIDBStorageRegistry::connection()andIDBStorageRegistry::transaction()getter. Those are convenient choke-points and it makes it way less likely we forget to add suchMESSAGE_CHECKwhen introducing new IPC.
Source/WebKit/NetworkProcess/storage/IDBStorageRegistry.cpp
Source/WebKit/NetworkProcess/storage/NetworkStorageManager.cpp
Patch Details
The change threads the sending IPC::Connection& into every IndexedDB identifier-to-object lookup in the network process, validates ownership at the two lookup choke-points, and promotes a release-inert assertion into a real message check.
In IDBStorageRegistry.cpp, ensureConnectionToClient() changes signature from (IPC::Connection::UniqueID, WebCore::IDBConnectionIdentifier) returning IDBConnectionToClient& to (IPC::Connection&, const WebCore::IDBResourceIdentifier&) returning a nullable IDBConnectionToClient*. It now MESSAGE_CHECKs that requestIdentifier.connectionIdentifier() is engaged and — critically — replaces the previous ASSERT(addResult.iterator->value->ipcConnection() == connection) with a real MESSAGE_CHECK_WITH_RETURN_VALUE(addResult.iterator->value->ipcConnection() == ipcConnection.uniqueID(), ...), so an existing registry entry owned by a different IPC connection now kills the sender instead of being silently reused. A new private helper isValidConnectionForIPC(UniqueIDBDatabaseConnection&, IPC::Connection&) maps a database connection back to its IDBConnectionToClient::identifier(), looks that up in m_connectionsToClient, and compares the stored ipcConnection() against the sender's uniqueID(); it returns true when no entry exists, a permissive default for the not-yet-registered case. connection(IDBDatabaseConnectionIdentifier) and transaction(IDBResourceIdentifier) gain an IPC::Connection& parameter, return RefPtr<...> instead of raw pointers, and run the ownership check before handing the object back; transaction() additionally MESSAGE_CHECKs that the identifier carries a connection identifier, a check moved down from the individual handlers.
In NetworkStorageManager.cpp/.h, exactly thirteen IDB message handlers that previously had no IPC::Connection& parameter at all (establishTransaction, databaseConnectionPendingClose, abortOpenAndUpgradeNeeded, didFireVersionChangeEvent, didGenerateIndexKeyForRecord, didFinishHandlingVersionChangeTransaction, clearObjectStore, getRecord, getAllRecords, getCount, deleteRecord, openCursor, iterateCursor) gain one and forward it into the registry; the remaining touched handlers already received the connection and only gain a forwarded argument. The private helper idbTransaction(const IDBRequestData&) gains an IPC::Connection& parameter. openDatabase, deleteDatabase and getAllDatabaseNamesAndVersions switch from Ref connectionToClient = ... to a nullable RefPtr with an early return. In WebCore, IDBConnectionToClient::identifier() and UniqueIDBDatabaseTransaction::databaseConnection() gain WEBCORE_EXPORT so the new WebKit-layer validation can call them.
Resolving a caller-supplied identifier against a shared registry without binding the lookup to the requesting principal's identity.
Background
Where this lives. IndexedDB data lives in the network process. Each WebContent process talks to NetworkStorageManager — an IPC::WorkQueueMessageReceiver servicing IDB messages from every WebContent process in a session — over IPC. The WebCore IDBServer classes (UniqueIDBDatabase, UniqueIDBDatabaseConnection, UniqueIDBDatabaseTransaction) do the actual work, and IDBStorageRegistry is the WebKit-layer bookkeeping that maps wire identifiers to those server-side objects.
Identifier types. IDBConnectionIdentifier is an alias for WebCore::ProcessIdentifier — it names one WebContent process's IDB client connection. IDBResourceIdentifier, used for transactions and requests, is a pair of Markable<IDBConnectionIdentifier> plus a Markable<IDBResourceObjectIdentifier>, where the latter is an AtomicObjectIdentifier. IDBDatabaseConnectionIdentifier names one open database connection.
IDBStorageConnectionToClient. The network-process delegate object implementing IDBConnectionToClientDelegate. It stores m_connection (an IPC::Connection::UniqueID), m_identifier (the IDBConnectionIdentifier), and owns a Ref<IDBConnectionToClient>. Every reply — didGetRecord, didGetAllRecords, didOpenCursor, didIterateCursor, didPutOrAdd, fireVersionChangeEvent — is dispatched through this delegate over m_connection.
The MESSAGE_CHECK idiom. WebKit's IPC validation macro family. MESSAGE_CHECK_BASE(assertion, connection) treats a failed assertion as a malformed or malicious message: it terminates the offending WebContent process rather than continuing. MESSAGE_CHECK_WITH_RETURN_VALUE_BASE is the variant for functions with a non-void return, returning the supplied value on failure. ASSERT, by contrast, is a debug-only macro that compiles to nothing in release builds.
The IPC threat model. A WebContent process is treated as potentially compromised. Every field of every message it sends — including identifiers that in benign operation are allocated by the client library — is attacker-chosen; the network process must not treat them as trustworthy.
Per-connection site validation. NetworkStorageManager::isSiteAllowedForConnection(IPC::Connection::UniqueID, RegistrableDomain) gates which origins a given WebContent connection may touch. It is invoked from openDatabase, deleteDatabase, and getAllDatabaseNamesAndVersions.
Analysis
This is IPC identifier confusion — a cross-process authorization failure, not memory corruption.
WebContent A (compromised) NetworkProcess WebContent B (victim)
────────────────────────── ────────────── ─────────────────────
getRecord(txnID_of_B) ─────────► m_transactions.get(txnID)
│ (no sender check)
▼
UniqueIDBDatabaseTransaction ◄── owned by B
│
└─► getRecord/putOrAdd/abort executed
reply routed via B's delegate ──────► B
── BOUNDARY CROSSED: A drove an operation on B's transaction ──
IDBStorageRegistry holds m_connectionsToClient keyed by IDBConnectionIdentifier (itself a ProcessIdentifier), m_connections keyed by IDBDatabaseConnectionIdentifier, and m_transactions keyed by IDBResourceIdentifier — all shared across every WebContent connection served by the same NetworkStorageManager. The lookup functions connection() and transaction() were pure map lookups: they took only an identifier extracted from an incoming message and returned whatever object was registered under it, with no parameter for — and therefore no possibility of — a check against the connection the message arrived on. Thirteen of the IDB handlers did not even receive an IPC::Connection&, so ownership validation was structurally impossible at those call sites.
Only weaker checks existed. Five handlers carried an engaged-optional check on the identifier — MESSAGE_CHECK(transactionIdentifier.connectionIdentifier(), connection) in abortTransaction and commitTransaction, and the requestIdentifier equivalents in openDatabase, deleteDatabase and getAllDatabaseNamesAndVersions — all of which merely verify that the optional is engaged, never that its value matches the sender. And in ensureConnectionToClient there was an ASSERT(addResult.iterator->value->ipcConnection() == connection), an assertion that compiles out in release builds, guarding precisely the mismatch case.
Two distinct confusions follow. The first is a connection-to-client hijack: ensureConnectionToClient does m_connectionsToClient.add(identifier, nullptr) and, only on isNewEntry, constructs an IDBStorageConnectionToClient bound to the sender's uniqueID(). If the entry already exists under some other process's identifier, the pre-existing entry is returned regardless of who sent the message; conversely, a process that registers first under an identifier it does not own owns that mapping for everyone. Since that delegate holds the IPC::Connection::UniqueID used to send every reply, the reply routing of an entire IDB client connection follows whichever process created the map entry. The second is object reference across connections: connection()/transaction() resolved another process's UniqueIDBDatabaseConnection or UniqueIDBDatabaseTransaction by identifier alone, letting the sender drive abort(), commit(), putOrAdd(), clearObjectStore(), deleteRecord(), establishTransaction(), cursor iteration, and version-change bookkeeping on transactions belonging to a different WebContent process. Because the origin and site checks are applied only on the open and delete paths and not on identifier-driven operations, this path bypasses the per-connection site restriction entirely.
This is not reachable from ordinary web content: the identifiers are allocated by the IDB client library inside the WebContent process, and web-exposed JS has no way to place arbitrary values in the wire message. The attacker model is a WebContent process already under attacker control, sending crafted NetworkStorageManager IDB messages.
(a) Reply-routing hijack via ensureConnectionToClient. The pre-fix body created a new IDBStorageConnectionToClient(connection, identifier) on isNewEntry and otherwise reused the existing one, with only an ASSERT covering the mismatch. A compromised process that sends openDatabase or getAllDatabaseNamesAndVersions carrying a requestIdentifier.connectionIdentifier() equal to another process's ProcessIdentifier would, if it wins the isNewEntry race, insert a delegate whose m_connection is the attacker's IPC::Connection::UniqueID under the victim's key. Every later reply for that key — didGetRecord, didGetAllRecords, didOpenCursor, didIterateCursor — would then be dispatched to the attacker's connection. Two preconditions are needed and neither is established by the supplied context: the attacker must be able to predict or enumerate the victim's IDBConnectionIdentifier value (IDBResourceIdentifier.h shows only the ProcessIdentifier alias, not its allocation or predictability), and the attacker's add() must land before the victim's own first ensureConnectionToClient call. If both hold, this would give cross-process disclosure of IDB read results — the path that most directly matches the commit message's "data leakage" wording, and precisely what the new MESSAGE_CHECK_WITH_RETURN_VALUE now forbids.
(b) Direct operation on another process's transaction or connection. With transaction() and connection() being bare m_transactions.get(identifier) / m_connections.get(identifier) lookups, a compromised process could name a victim's IDBResourceIdentifier and drive putOrAdd, deleteRecord, clearObjectStore, abort, commit, openCursor/iterateCursor, or establishTransaction against it. This direction needs the attacker to guess a live handle: the resource half is an AtomicObjectIdentifier<IDBResourceObjectIdentifierType> and the connection half is the victim's ProcessIdentifier, but the supplied context does not show either allocation site, so the size of the search space remains unestablished. Read results on this path would be delivered to the victim's delegate rather than the attacker's, so on its own this direction would be mutation and denial — corrupting or aborting another site's transactions — rather than disclosure. Chained with (a), the read results could be redirected to the attacker's connection, turning it into cross-site record disclosure.
Note the permissive default the fix retains in isValidConnectionForIPC: when no m_connectionsToClient entry exists for the database connection's client identifier, it returns true. That leaves the not-yet-registered window as the interesting corner for follow-up review.
The vulnerable code runs in the NetworkProcess on NetworkStorageManager's SuspendableWorkQueue. Exploiting it does not itself escape any sandbox — it requires a prior WebContent compromise and would yield lateral access to other WebContent processes' stored data within the same session.
Discovery most likely came from targeted pattern auditing of the network-process IPC surface rather than fuzzing. The shape of the patch — adding IPC::Connection& to thirteen handlers that previously had no way to reference the sender — is what a systematic sweep for "IPC handlers that consume an identifier but never look at the connection" produces. The ASSERT sitting on the exact violated condition in ensureConnectionToClient is a strong candidate for the entry point: auditing ASSERTs over IPC-derived state is a known-productive heuristic, and the reviewer credit plus the explicit rationale in the commit message ("convenient choke-points... less likely we forget") reads as a deliberate hardening pass. The rdar:// reference and the rapid-branch landing suggest internal discovery rather than an external report.
No memory-safety primitive is involved: the registry holds WeakPtrs and the fix returns RefPtrs, so this is an authorization failure, not a lifetime failure. This vulnerability weakens cross-process isolation at the network-process IPC boundary — the boundary that is supposed to keep one WebContent process's stored data out of reach of another. The security model assumes an IDB connection, database-connection, or transaction identifier is a capability scoped to the process that created it, and that the per-connection site restrictions enforced on openDatabase/deleteDatabase/getAllDatabaseNamesAndVersions therefore cover all subsequent operations reached through those identifiers; before the fix, identifier-driven operations were authorized by identifier possession alone. An attacker who has already compromised one WebContent process could, per the commit message's "data leakage" characterization, reach IndexedDB state belonging to another WebContent process — reading and mutating records of origins the compromised process was never permitted to open, and redirecting another process's IDB reply traffic to itself.
Insight
The interesting artifact is the ASSERT that was already sitting on the exact condition the attacker violates: ASSERT(addResult.iterator->value->ipcConnection() == connection). Somebody knew the invariant, encoded it, and encoded it in the one form that evaporates in shipping builds. An ASSERT on an IPC-derived value is a code smell worth grepping for as a class — an assertion on attacker-controlled data is either a MESSAGE_CHECK in disguise or dead code.
The second point is architectural, and it is what the commit message calls out: the pre-fix design put the ownership check, where there was one, at the message handler — of which there are dozens — rather than at the lookup, of which there are two. That shape guarantees drift; thirteen handlers in this file did not even take an IPC::Connection&, so the check could not have been written at those sites without a signature change. Moving validation into connection()/transaction() converts "remember to check in each new IPC handler" into "you cannot obtain the object without passing the connection", which is the type-system-shaped version of the fix. Note also that this fix scopes to IPC connections, not origins: two same-process cross-origin frames still share an IDBConnectionIdentifier, since it is a ProcessIdentifier, so this check is a process-isolation boundary rather than an origin boundary.
Audit directions
-
Shared identifier-to-object registries whose lookup API does not require the caller to present the requesting principal — possession of a name becomes possession of the capability. Narrow: grep
Source/WebKit/NetworkProcessandSource/WebKit/GPUProcessforHashMap<...Identifier, ...>members whose getters take only an identifier — start with the other registries alongside this one (CacheStorageRegistry,FileSystemStorageHandleRegistry,StorageAreaRegistry) and check whether their getters accept anIPC::Connection&. Match tell (narrow): a getter signature of the formX* get(SomeIdentifier)called directly from anIPC::MessageReceiverhandler, with no connection parameter anywhere in the call chain. Wider: the same class shows up wherever a shared broker maps opaque handles to objects across mutually distrusting clients — GPU-process remote-object registries, ServiceWorker registration and fetch-identifier tables, WebAudio/MediaStream track handles. Match tell (wider): anyfind(identifier)in a broker whose result is used before any comparison against the sender. Widest: this is the classic confused-deputy / IDOR shape and the invariant is a name is not an authorization; every lookup keyed by caller-supplied data must be re-anchored to the caller's identity at the lookup itself — it applies to Chromium Mojo interfaces keyed by client-supplied IDs, to kernel fd and handle tables, and to any REST service resolving/resource/:idwithout an ownership join. Match tell (widest): the authorization predicate and the lookup live in different functions. -
Release-build-inert assertions placed on IPC-derived values.
ASSERT,ASSERT_WITH_MESSAGE,ASSERT_UNUSED, and similar checks whose operand traces back to a message parameter. This bug shipped withASSERT(addResult.iterator->value->ipcConnection() == connection)guarding exactly the exploited condition. Narrow: grepSource/WebKit/NetworkProcessandSource/WebKit/GPUProcessforASSERT(inside functions whose parameters includeIPC::Connection,...Identifier, or a decoded message struct, and triage each for whether it should beMESSAGE_CHECK. Match tell (narrow): the asserted expression names both a caller-supplied identifier and a server-side state value. Wider: the same class covers any validation expressed in a construct the shipping build discards —NDEBUG-gated checks, debug-only logging that would have surfaced the mismatch,#if ASSERT_ENABLEDblocks. Widest: the invariant is security validation must not be compiled out by build configuration, and the same audit applies to Rustdebug_assert!on deserialized input, Javaasserton RPC payloads, and Pythonassertstatements erased under-O. Match tell (widest): a check whose presence depends on a build flag but whose absence is attacker-observable. -
Investigate the permissive default the fix deliberately keeps.
isValidConnectionForIPCreturnstruewhenm_connectionsToClient.find(connectionIdentifier)misses. Trace which lifecycle windows produce that miss — the interval before a process's firstensureConnectionToClientcall, and the interval afterremoveConnectionToClienthas torn down entries for a closing connection whilem_connectionsandm_transactionsmay still hold liveWeakPtrs. Match tell: any state in which aUniqueIDBDatabaseConnectionis reachable fromm_connectionsbut its client identifier has nom_connectionsToCliententry — in that state the new check is a no-op and the pre-fix behavior persists. Verification here is nontrivial and likely needs a two-process IPC harness rather than static reading, since it is a teardown-ordering question. -
Authorization enforced on the setup path but not on the identifier-driven paths that follow it. In this file,
isSiteAllowedForConnectiongatesopenDatabase,deleteDatabase, andgetAllDatabaseNamesAndVersions, but the two dozen operations reached through a transaction identifier inherit no origin check at all. Narrow: enumerate everyMESSAGE_CHECK(isSiteAllowedForConnection(...))call site inNetworkStorageManager.cppand, for each resource created behind it, list the operations reachable on that resource by identifier alone — verify each of those either re-derives the origin or is provably confined by the setup check. Wider: the same asymmetry appears anywhere an open or create call is policy-checked and the returned handle is then used unchecked — file-system handle APIs, cache-storage record access, ServiceWorker client handles. Match tell (wider): a policy predicate that appears inopen/create/ensurehandlers and in no other handler of the same subsystem. Widest: the invariant is if a policy applies to a resource, it must be evaluated per operation or provably implied by the handle's provenance — the same reasoning covers POSIX open-time permission checks versus fd inheritance, OAuth scope checked at token issuance but not per call, and capability handles passed across trust boundaries.