← All reports

[5] IndexedDB Connection/Transaction Identifier Confusion

MediumWebKit NetworkProcess storageAuthBypass

IndexedDB handed out other processes' transactions to whoever named them.

d854553

Medium입니다. 이 버그는 손상된 renderer에게 다른 process의 IndexedDB transaction을 그대로 넘겨줍니다. 읽기, 쓰기, 삭제가 모두 가능하며, connection별 site 제한이 완전히 우회됩니다. High band로 분류되지 않은 이유는 해당 동작이 철저히 post-compromise 상태에서만 성립하고, 유출 시나리오의 절반은 identifier를 추측해야 하는데 이 change 자체가 그 예측 가능성을 입증하지는 않기 때문입니다.

공유 broker에서의 cross-process authorization은 하나의 질문으로 귀결됩니다. 이름을 알고 있다는 것이 곧 그 대상을 소유했다는 뜻인가 하는 질문입니다. WebKit의 IndexedDB 데이터는 network process에 존재하며, 이 process는 한 세션 안의 모든 WebContent process가 보내는 IDB 메시지를 처리합니다. 따라서 identifier와 object를 매핑하는 registry는 서로를 신뢰하지 않는 여러 client 사이에서 공유되는 구조입니다. 이 threat model에서는 WebContent process가 언제든 compromise될 수 있다고 가정하며, 그렇다는 것은 해당 process가 보내는 메시지의 모든 필드가 공격자에 의해 조작될 수 있다는 의미입니다. 이 전제에서 반드시 지켜져야 할 invariant는, IPC 메시지에 실린 identifier가 오직 그것을 보낸 connection이 소유한 object로만 resolve되어야 한다는 것입니다.

관전 포인트: 공격자가 이미 장악한 WebContent process는 다른 process의 IDB transaction을 이름으로 지목할 수 있고, 이를 통해 애초에 열 권한이 없었던 origin을 대상으로 읽기, 쓰기, abort, object-store 삭제까지 실행할 수 있습니다.

From the commit message:

NetworkStorageManager fails 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_CHECK calls inside the IDBStorageRegistry::connection() and IDBStorageRegistry::transaction() getter. Those are convenient choke-points and it makes it way less likely we forget to add such MESSAGE_CHECK when introducing new IPC.

Source/WebKit/NetworkProcess/storage/IDBStorageRegistry.cpp

+#define MESSAGE_CHECK_WITH_RETURN_VALUE(assertion, connection, returnValue) MESSAGE_CHECK_WITH_RETURN_VALUE_BASE(assertion, connection, returnValue)
 
-WebCore::IDBServer::IDBConnectionToClient& IDBStorageRegistry::ensureConnectionToClient(IPC::Connection::UniqueID connection, WebCore::IDBConnectionIdentifier identifier)
+WebCore::IDBServer::IDBConnectionToClient* IDBStorageRegistry::ensureConnectionToClient(IPC::Connection& ipcConnection, const WebCore::IDBResourceIdentifier& requestIdentifier)
{
+ MESSAGE_CHECK_WITH_RETURN_VALUE(requestIdentifier.connectionIdentifier(), ipcConnection, nullptr);
+ auto identifier = *requestIdentifier.connectionIdentifier();
auto addResult = m_connectionsToClient.add(identifier, nullptr);
if (addResult.isNewEntry)
 
- addResult.iterator->value = makeUnique<IDBStorageConnectionToClient>(connection, identifier);
+ addResult.iterator->value = makeUnique<IDBStorageConnectionToClient>(ipcConnection.uniqueID(), identifier);
 
- ASSERT(addResult.iterator->value->ipcConnection() == connection);
 
- return addResult.iterator->value->connectionToClient();
+ MESSAGE_CHECK_WITH_RETURN_VALUE(addResult.iterator->value->ipcConnection() == ipcConnection.uniqueID(), ipcConnection, nullptr);
+ return &addResult.iterator->value->connectionToClient();
}
 
-WebCore::IDBServer::UniqueIDBDatabaseConnection* IDBStorageRegistry::connection(WebCore::IDBDatabaseConnectionIdentifier identifier)
+bool IDBStorageRegistry::isValidConnectionForIPC(WebCore::IDBServer::UniqueIDBDatabaseConnection& databaseConnection, IPC::Connection& ipcConnection)
{
 
- return m_connections.get(identifier);
+ auto connectionIdentifier = databaseConnection.connectionToClient().identifier();
+ auto it = m_connectionsToClient.find(connectionIdentifier);
+ if (it == m_connectionsToClient.end())
+ return true;
+ return it->value->ipcConnection() == ipcConnection.uniqueID();
}
 
-WebCore::IDBServer::UniqueIDBDatabaseTransaction* IDBStorageRegistry::transaction(WebCore::IDBResourceIdentifier identifier)
+SUPPRESS_NODELETE RefPtr<WebCore::IDBServer::UniqueIDBDatabaseConnection> IDBStorageRegistry::connection(WebCore::IDBDatabaseConnectionIdentifier identifier, IPC::Connection& ipcConnection)
{
+ RefPtr databaseConnection = m_connections.get(identifier);
+ if (!databaseConnection)
+ return nullptr;
+ MESSAGE_CHECK_WITH_RETURN_VALUE(isValidConnectionForIPC(*databaseConnection, ipcConnection), ipcConnection, nullptr);
+ return databaseConnection;
+}
+
+SUPPRESS_NODELETE RefPtr<WebCore::IDBServer::UniqueIDBDatabaseTransaction> IDBStorageRegistry::transaction(WebCore::IDBResourceIdentifier identifier, IPC::Connection& ipcConnection)
+{
+ MESSAGE_CHECK_WITH_RETURN_VALUE(identifier.connectionIdentifier(), ipcConnection, nullptr);
if (identifier.isEmpty())
return nullptr;
 
- return m_transactions.get(identifier);
+ RefPtr transaction = m_transactions.get(identifier);
+ if (!transaction)
+ return nullptr;
+ if (RefPtr databaseConnection = transaction->databaseConnection())
+ MESSAGE_CHECK_WITH_RETURN_VALUE(isValidConnectionForIPC(*databaseConnection, ipcConnection), ipcConnection, nullptr);
+ return transaction;
}

Source/WebKit/NetworkProcess/storage/NetworkStorageManager.cpp

-void NetworkStorageManager::getRecord(const WebCore::IDBRequestData& requestData, const WebCore::IDBGetRecordData& getRecordData)
+void NetworkStorageManager::getRecord(IPC::Connection& connection, const WebCore::IDBRequestData& requestData, const WebCore::IDBGetRecordData& getRecordData)
{
 
- if (RefPtr transaction = idbTransaction(requestData))
+ if (RefPtr transaction = idbTransaction(requestData, connection))
transaction->getRecord(requestData, getRecordData);
}
...
-void NetworkStorageManager::establishTransaction(WebCore::IDBDatabaseConnectionIdentifier databaseConnectionIdentifier, const WebCore::IDBTransactionInfo& transactionInfo)
+void NetworkStorageManager::establishTransaction(IPC::Connection& ipcConnection, WebCore::IDBDatabaseConnectionIdentifier databaseConnectionIdentifier, const WebCore::IDBTransactionInfo& transactionInfo)
{
 
- if (RefPtr connection = m_idbStorageRegistry->connection(databaseConnectionIdentifier))
+ if (RefPtr connection = m_idbStorageRegistry->connection(databaseConnectionIdentifier, ipcConnection))
connection->establishTransaction(transactionInfo);
}

이번 변경은 네트워크 프로세스 내 모든 IndexedDB 식별자-객체 조회 지점에 발신 측 IPC::Connection&을 함께 전달하도록 만들고, 두 핵심 조회 지점에서 소유권을 검증하며, release 빌드에서는 무력화되는 assertion을 실제 메시지 검증으로 격상시킵니다.

IDBStorageRegistry.cpp에서 ensureConnectionToClient()의 시그니처가 (IPC::Connection::UniqueID, WebCore::IDBConnectionIdentifier)를 받아 IDBConnectionToClient&를 반환하던 형태에서, (IPC::Connection&, const WebCore::IDBResourceIdentifier&)를 받아 nullable한 IDBConnectionToClient*를 반환하는 형태로 바뀌었습니다. 이제 requestIdentifier.connectionIdentifier()가 engaged 상태인지를 MESSAGE_CHECK로 검사하며, 무엇보다 기존의 ASSERT(addResult.iterator->value->ipcConnection() == connection)을 실제 MESSAGE_CHECK_WITH_RETURN_VALUE(addResult.iterator->value->ipcConnection() == ipcConnection.uniqueID(), ...)로 교체했습니다. 그 결과 다른 IPC connection이 소유한 기존 registry entry를 조용히 재사용하는 대신, 발신자를 종료시키는 동작으로 바뀌었습니다. 새로 추가된 private 헬퍼 isValidConnectionForIPC(UniqueIDBDatabaseConnection&, IPC::Connection&)는 database connection을 그 IDBConnectionToClient::identifier()로 역매핑한 뒤, 이를 m_connectionsToClient에서 조회하여 저장된 ipcConnection()을 발신자의 uniqueID()와 비교합니다. entry가 존재하지 않는 경우에는 true를 반환하는데, 이는 아직 등록되지 않은 상태에 대한 허용적 기본값입니다. connection(IDBDatabaseConnectionIdentifier)transaction(IDBResourceIdentifier)IPC::Connection& 파라미터를 추가로 받고, raw pointer 대신 RefPtr<...>를 반환하며, 객체를 반환하기 전에 소유권 검사를 수행합니다. transaction()은 여기에 더해 식별자가 connection identifier를 담고 있는지를 MESSAGE_CHECK하는데, 이는 각 핸들러에 개별적으로 있던 검사를 아래로 옮긴 것입니다.

NetworkStorageManager.cpp/.h에서는 이전까지 IPC::Connection& 파라미터를 전혀 받지 않던 열세 개의 IDB 메시지 핸들러(establishTransaction, databaseConnectionPendingClose, abortOpenAndUpgradeNeeded, didFireVersionChangeEvent, didGenerateIndexKeyForRecord, didFinishHandlingVersionChangeTransaction, clearObjectStore, getRecord, getAllRecords, getCount, deleteRecord, openCursor, iterateCursor)가 해당 파라미터를 새로 받아 registry로 전달하도록 바뀌었습니다. 그 밖에 수정된 핸들러들은 이미 connection을 받고 있었고, 여기에 전달용 인자만 추가되었습니다. private 헬퍼 idbTransaction(const IDBRequestData&)IPC::Connection& 파라미터를 새로 받습니다. openDatabase, deleteDatabase, getAllDatabaseNamesAndVersionsRef connectionToClient = ... 형태에서 nullable한 RefPtr과 조기 반환(early return)을 사용하는 형태로 바뀌었습니다. WebCore 쪽에서는 IDBConnectionToClient::identifier()UniqueIDBDatabaseTransaction::databaseConnection()WEBCORE_EXPORT가 추가되어, 새로 도입된 WebKit 레이어의 검증 코드에서 이들을 호출할 수 있게 되었습니다.

발신자의 identity와 lookup을 연결하지 않은 채, 호출자가 제공한 식별자를 공유 registry에서 그대로 조회하는 패턴.

이 코드가 있는 위치. IndexedDB 데이터는 network process에 저장됩니다. 각 WebContent process는 세션 내 모든 WebContent process의 IDB 메시지를 처리하는 IPC::WorkQueueMessageReceiverNetworkStorageManager와 IPC를 통해 통신합니다. 실제 작업은 WebCore의 IDBServer 클래스들(UniqueIDBDatabase, UniqueIDBDatabaseConnection, UniqueIDBDatabaseTransaction)이 수행하며, IDBStorageRegistry는 wire identifier를 이 서버 측 객체들에 매핑하는 WebKit 레이어의 장부 역할을 담당합니다.

식별자 타입. IDBConnectionIdentifierWebCore::ProcessIdentifier의 alias로, 하나의 WebContent process가 가진 IDB client connection을 가리킵니다. transaction과 request에 쓰이는 IDBResourceIdentifierMarkable<IDBConnectionIdentifier>Markable<IDBResourceObjectIdentifier>의 쌍이며, 후자는 AtomicObjectIdentifier입니다. IDBDatabaseConnectionIdentifier는 열려 있는 하나의 database connection을 가리킵니다.

IDBStorageConnectionToClient. IDBConnectionToClientDelegate를 구현하는 network-process 측 delegate 객체입니다. m_connection(IPC::Connection::UniqueID), m_identifier(IDBConnectionIdentifier)를 저장하고, Ref<IDBConnectionToClient>를 소유합니다. didGetRecord, didGetAllRecords, didOpenCursor, didIterateCursor, didPutOrAdd, fireVersionChangeEvent 등 모든 응답이 이 delegate를 거쳐 m_connection을 통해 전달됩니다.

MESSAGE_CHECK idiom. WebKit의 IPC validation 매크로 계열입니다. MESSAGE_CHECK_BASE(assertion, connection)는 assertion 실패를 malformed 또는 malicious message로 간주하여, 계속 진행하는 대신 문제를 일으킨 WebContent process를 종료시킵니다. MESSAGE_CHECK_WITH_RETURN_VALUE_BASE는 non-void 반환값을 갖는 함수용 variant로, 실패 시 지정된 값을 반환합니다. 반면 ASSERT는 debug 빌드 전용 매크로로, release 빌드에서는 아무 코드도 생성하지 않습니다.

IPC threat model. WebContent process는 이미 compromise되었을 가능성이 있는 대상으로 취급됩니다. 정상 동작 시에는 client library가 할당하는 식별자를 포함해, WebContent process가 보내는 메시지의 모든 필드는 attacker-chosen으로 간주되며, network process는 이를 신뢰할 수 있는 값으로 다루어서는 안 됩니다.

Per-connection site validation. NetworkStorageManager::isSiteAllowedForConnection(IPC::Connection::UniqueID, RegistrableDomain)은 특정 WebContent connection이 접근할 수 있는 origin을 제한하는 역할을 하며, openDatabase, deleteDatabase, getAllDatabaseNamesAndVersions에서 호출됩니다.

이번 사안은 memory corruption이 아니라 IPC identifier confusion, 즉 프로세스 간 authorization 실패에 해당합니다.

  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 ──

IDBStorageRegistryIDBConnectionIdentifier(사실상 ProcessIdentifier)로 키를 만든 m_connectionsToClient, IDBDatabaseConnectionIdentifier로 키를 만든 m_connections, IDBResourceIdentifier로 키를 만든 m_transactions를 보유하고 있는데, 이들은 모두 같은 NetworkStorageManager가 서비스하는 모든 WebContent connection에 걸쳐 공유됩니다. 조회 함수인 connection()transaction()은 순수한 map lookup에 불과했습니다. 즉 들어오는 메시지에서 추출한 식별자만 받아, 그 식별자로 등록된 객체를 그대로 반환했으며, 메시지가 도착한 connection과 대조할 파라미터 자체가 없었기 때문에 애초에 그런 검사가 불가능한 구조였습니다. 열세 개의 IDB 핸들러는 IPC::Connection&조차 받지 않았으므로, 해당 호출 지점에서는 소유권 검증이 구조적으로 불가능했습니다.

존재했던 것은 더 약한 검사뿐이었습니다. abortTransactioncommitTransactionMESSAGE_CHECK(transactionIdentifier.connectionIdentifier(), connection), 그리고 openDatabase, deleteDatabase, getAllDatabaseNamesAndVersions의 동등한 requestIdentifier 검사 등 다섯 개 핸들러에 식별자의 engaged 여부만 확인하는 optional 검사가 있었을 뿐, 그 값이 발신자와 일치하는지는 전혀 검증하지 않았습니다. 그리고 ensureConnectionToClient에는 바로 그 불일치 상황을 막기 위한 ASSERT(addResult.iterator->value->ipcConnection() == connection)이 있었지만, 이는 release 빌드에서 사라지는 assertion이었습니다.

여기서 두 가지 서로 다른 confusion이 발생합니다. 첫 번째는 connection-to-client hijack입니다. ensureConnectionToClientm_connectionsToClient.add(identifier, nullptr)를 수행하고, isNewEntry인 경우에만 발신자의 uniqueID()에 바인딩된 IDBStorageConnectionToClient를 새로 생성합니다. 만약 해당 entry가 다른 process의 식별자로 이미 존재한다면, 실제 발신자와 무관하게 기존 entry가 그대로 반환됩니다. 반대로, 어떤 identifier를 소유하지 않은 process가 그 identifier로 먼저 등록해버리면, 이후 그 매핑은 모두에게 그 process의 소유가 됩니다. 이 delegate가 모든 응답을 전송하는 데 쓰이는 IPC::Connection::UniqueID를 들고 있기 때문에, 하나의 IDB client connection 전체에 대한 응답 라우팅이 결국 어느 process가 map entry를 먼저 생성했는지에 따라 결정됩니다. 두 번째는 object reference across connections입니다. connection()/transaction()은 오직 식별자만으로 다른 process의 UniqueIDBDatabaseConnection이나 UniqueIDBDatabaseTransaction을 조회했으며, 이로 인해 발신자가 다른 WebContent process 소유의 transaction에 대해 abort(), commit(), putOrAdd(), clearObjectStore(), deleteRecord(), establishTransaction(), cursor iteration, version-change bookkeeping을 수행할 수 있었습니다. origin과 site 검사가 open/delete 경로에서만 적용되고 identifier 기반 조작 경로에는 적용되지 않았기 때문에, 이 경로는 per-connection site restriction을 완전히 우회합니다.

이 경로는 일반적인 웹 콘텐츠에서는 도달할 수 없습니다. 식별자는 WebContent process 내부의 IDB client library가 할당하는 값이며, 웹에 노출된 JS가 wire message에 임의의 값을 직접 넣을 방법이 없기 때문입니다. 여기서 가정하는 공격자 모델은 이미 attacker control 하에 놓인 WebContent process가 조작된 NetworkStorageManager IDB 메시지를 보내는 상황입니다.

(a) ensureConnectionToClient를 통한 reply-routing hijack. 패치 이전 코드는 isNewEntry일 때만 새 IDBStorageConnectionToClient(connection, identifier)를 생성했고, 그렇지 않으면 기존 것을 재사용했으며, 불일치 여부는 오직 ASSERT만이 다루고 있었습니다. compromise된 process가 openDatabasegetAllDatabaseNamesAndVersions를 보내면서 requestIdentifier.connectionIdentifier()다른 process의 ProcessIdentifier 값으로 설정한다면, isNewEntry 경쟁에서 이긴 경우 공격자의 IPC::Connection::UniqueIDm_connection으로 갖는 delegate가 피해자의 키 아래 삽입될 수 있습니다. 이후 해당 키에 대한 모든 응답 — didGetRecord, didGetAllRecords, didOpenCursor, didIterateCursor — 이 공격자의 connection으로 전달됩니다. 다만 이 시나리오가 성립하려면 두 가지 전제조건이 필요하며, 제공된 context만으로는 둘 다 확인되지 않습니다. 먼저 공격자가 피해자의 IDBConnectionIdentifier 값을 예측하거나 열거할 수 있어야 하는데(IDBResourceIdentifier.h에는 ProcessIdentifier alias만 나타나 있을 뿐, 그 할당 방식이나 예측 가능성은 드러나 있지 않습니다), 또한 공격자의 add() 호출이 피해자 자신의 첫 ensureConnectionToClient 호출보다 먼저 실행되어야 합니다. 두 조건이 모두 성립한다면, 프로세스 간 IDB 읽기 결과 노출로 이어질 가능성이 있습니다. 이는 commit message의 "data leakage"라는 표현과 가장 직접적으로 맞닿는 경로이며, 새로 도입된 MESSAGE_CHECK_WITH_RETURN_VALUE가 정확히 막고자 하는 상황이기도 합니다.

(b) 다른 process의 transaction 또는 connection에 대한 직접 조작. transaction()connection()이 단순한 m_transactions.get(identifier) / m_connections.get(identifier) 조회에 불과했기 때문에, compromise된 process가 피해자의 IDBResourceIdentifier를 지정하여 putOrAdd, deleteRecord, clearObjectStore, abort, commit, openCursor/iterateCursor, establishTransaction을 그 대상에 대해 실행시킬 수 있었습니다. 이 방향의 공격은 공격자가 살아있는 handle을 추측해야 합니다. resource 쪽은 AtomicObjectIdentifier<IDBResourceObjectIdentifierType>이고 connection 쪽은 피해자의 ProcessIdentifier인데, 제공된 context에는 둘 중 어느 쪽의 할당 방식도 나타나 있지 않아 탐색 공간의 크기는 확인되지 않습니다. 이 경로에서의 읽기 결과는 공격자가 아니라 피해자의 delegate로 전달되므로, 이 방향만 놓고 보면 disclosure가 아니라 다른 site의 transaction을 손상시키거나 중단시키는 mutation과 denial에 가깝습니다. 다만 (a)와 연쇄될 경우, 읽기 결과가 공격자의 connection으로 리다이렉트되어 cross-site record disclosure로 이어질 가능성이 있습니다.

패치에서도 여전히 남아 있는 permissive default 하나를 짚어둘 필요가 있습니다. isValidConnectionForIPC는 database connection의 client identifier에 대응하는 m_connectionsToClient entry가 존재하지 않을 경우 true를 반환합니다. 아직 등록되지 않은 이 시점의 window가 후속 검토에서 흥미롭게 살펴볼 지점으로 남습니다.

취약한 코드는 NetworkProcess 내 NetworkStorageManagerSuspendableWorkQueue에서 실행됩니다. 이를 악용한다고 해서 그 자체로 어떤 sandbox를 벗어나는 것은 아니며, 사전에 WebContent process가 compromise되어 있어야 하고, 그 결과로 같은 세션 내 다른 WebContent process가 저장한 데이터에 대한 lateral access를 얻게 됩니다.

발견 경로는 fuzzing보다는 network-process IPC 표면에 대한 표적화된 패턴 감사에서 비롯되었을 가능성이 높아 보입니다. 이전까지 발신자를 참조할 방법이 전혀 없던 열세 개 핸들러에 IPC::Connection&를 추가한 patch의 형태는, "identifier를 받아 처리하지만 connection은 전혀 확인하지 않는 IPC 핸들러"를 체계적으로 훑는 감사에서 나올 법한 결과물입니다. ensureConnectionToClient에서 위반된 조건 바로 위에 놓여 있던 ASSERT가 유력한 진입점으로 보입니다. IPC로부터 유입된 상태에 걸린 ASSERT를 점검하는 것은 이미 알려진 유효한 감사 휴리스틱이며, 리뷰어 크레딧과 commit message에 담긴 명시적인 근거("convenient choke-points... less likely we forget")는 의도적인 hardening 작업이었음을 시사합니다. rdar:// 참조와 빠른 branch 반영 속도로 미루어 보면, 외부 제보보다는 내부에서 발견되었을 가능성이 높아 보입니다.

memory-safety primitive는 관여하지 않습니다. registry가 보유한 것은 WeakPtr이고 patch는 RefPtr을 반환하므로, 이는 lifetime 실패가 아니라 authorization 실패입니다. 이 vulnerability는 network-process IPC 경계에서의 프로세스 간 isolation을 약화시키는데, 이 경계는 원래 한 WebContent process가 저장한 데이터를 다른 WebContent process가 건드리지 못하도록 지켜야 할 지점입니다. 보안 모델은 IDB connection, database-connection, transaction 식별자가 그것을 생성한 process에 scope된 capability이며, openDatabase/deleteDatabase/getAllDatabaseNamesAndVersions에서 적용되는 per-connection site restriction이 해당 식별자를 거쳐 이루어지는 이후의 모든 조작까지 커버한다는 전제 위에 서 있습니다. 그러나 패치 이전에는 identifier 기반 조작이 오직 identifier를 알고 있다는 사실만으로 authorize되고 있었습니다. commit message가 "data leakage"라고 표현한 대로, 이미 하나의 WebContent process를 compromise한 공격자는 다른 WebContent process에 속한 IndexedDB 상태에 도달할 수 있으며, compromise된 process가 애초에 열 수 없었던 origin의 record를 읽고 변경하거나, 다른 process의 IDB 응답 트래픽을 자신에게로 redirect할 수 있게 됩니다.

가장 흥미로운 지점은 공격자가 위반하는 바로 그 조건 위에 이미 놓여 있던 ASSERT입니다: ASSERT(addResult.iterator->value->ipcConnection() == connection). 누군가는 이 invariant를 이미 알고 있었고 그것을 코드로 표현했지만, shipping 빌드에서 사라지는 형태로 표현해버렸습니다. IPC로부터 유입된 값에 걸린 ASSERT는 하나의 category로서 검색해볼 가치가 있는 code smell입니다. attacker-controlled data에 대한 assertion은 사실상 위장된 MESSAGE_CHECK이거나, 아니면 죽은 코드일 수밖에 없기 때문입니다.

두 번째 지점은 구조적인 것이며, commit message 자체가 지적하고 있는 부분입니다. 패치 이전 설계는 소유권 검사가 존재하는 경우조차 그것을 수십 개에 달하는 message handler 쪽에 두었지, 단 두 곳뿐인 lookup 지점에 두지 않았습니다. 이런 구조는 필연적으로 drift를 만들어냅니다. 실제로 이 파일 안의 열세 개 핸들러는 애초에 IPC::Connection&조차 받지 않았기 때문에, 시그니처 변경 없이는 그 지점에 검사를 추가할 수조차 없었습니다. 검증 로직을 connection()/transaction() 내부로 옮긴 것은 "새 IPC 핸들러를 만들 때마다 검사를 기억해야 한다"는 요구를, "connection을 넘기지 않고서는 애초에 객체를 얻을 수 없다"는 요구로 바꾼 셈이며, 이는 타입 시스템 차원에서 강제되는 형태의 fix에 해당합니다. 다만 이 패치는 origin이 아니라 IPC connection 단위로 scope된다는 점도 짚어둘 필요가 있습니다. IDBConnectionIdentifierProcessIdentifier인 이상, 같은 process 안의 서로 다른 origin을 가진 두 프레임은 여전히 하나의 IDBConnectionIdentifier를 공유하므로, 이번 검사는 origin 경계가 아니라 process-isolation 경계에 해당합니다.