[1] CORS bypass via unvalidated SetCORSDisablingPatterns IPC
The CORS policy took a detour through the process it was written to contain.
High. CORS enforcement 여부를 network layer에 알려주는 policy가, 원래 그 policy로 제약해야 할 sandboxed process를 거쳐 enforcement point까지 전달되었습니다. Payload validation만으로는 이런 구조적 문제를 막을 수 없었습니다. 라우팅 자체가 버그였던 셈이며, code execution을 확보한 renderer는 메시지 한 번으로 인증된 모든 사이트를 universal하게 읽어낼 수 있는 경로를 갖게 되었습니다.
WebKit은 작업을 신뢰된 UIProcess, untrusted script를 실행하는 sandboxed WebContent process, 그리고 cookie jar를 소유하고 실제 HTTP load를 수행하는 NetworkProcess로 나누어 처리합니다. Embedder는 페이지별로 URL match pattern 목록 — _corsDisablingPatterns 설정 — 을 WebKit에 전달할 수 있으며, 이 패턴에 해당하는 URL에서는 CORS enforcement가 건너뛰어집니다. 이 목록은 renderer(WebCore 쪽의 same-origin check용)와 network layer(credential이 포함된 cross-origin response를 거부할지 결정하는 쪽) 양쪽 모두에 전달되어야 합니다. 보안상 기대되는 전제는, NetworkProcess에서 enforcement를 좌우하는 값이 최소한 그 enforcement point만큼의 privilege를 가진 process에 의해서만 attest되어야 한다는 것입니다.
관전 포인트: 이미 code execution을 확보한 renderer는 *://*/*를 선언하는 메시지 하나만 보내면, network layer가 모든 URL에 대해 CORS enforcement를 중단하도록 만들 수 있습니다. 결과적으로 renderer compromise가, 사용자가 로그인된 모든 사이트를 읽어낼 수 있는 universal read로 확장됩니다.
Commit message에는 패치 이전 경로가 명확히 기술되어 있습니다. _corsDisablingPatterns는 UIProcess에서 WebContent process를 거쳐 Messages::NetworkConnectionToWebProcess::SetCORSDisablingPatterns를 통해 NetworkProcess로 전달되었습니다. 이 구조에서는 compromise된 WebContent process가 공격자가 원하는 pattern을 담아 해당 IPC를 보내면, 임의의 cross-origin URL에 대해 CORS를 비활성화하고 사용자가 인증된 모든 사이트의 content를 읽어낼 수 있었습니다. 패치는 pattern을 UIProcess에서 NetworkProcess로 직접 전달하도록 라우팅을 변경해 WebContent process를 trust path에서 제외했습니다. 다만 Messages::WebPage::UpdateCORSDisablingPatterns는 그대로 유지되어, renderer가 자신의 in-process origin-access pattern은 계속 채울 수 있습니다.
Source/WebKit/NetworkProcess/NetworkProcess.cpp
Source/WebKit/WebProcess/WebPage/WebPage.cpp
Patch Details
이번 변경은 WebContent에서 Networking으로 향하던 전달 경로를 제거하고, 대신 UIProcess에서 Networking으로 향하는 경로를 새로 추가합니다. 아울러 아직 NetworkProcess가 존재하지 않는 경우를 위해, 패턴 목록이 connection 생성 파라미터를 통해 함께 전달되도록 합니다.
제거된 항목은 다음과 같습니다. NetworkConnectionToWebProcess.messages.in의 SetCORSDisablingPatterns(WebCore::PageIdentifier, Vector<String>) 엔트리(DispatchedFrom=WebContent, DispatchedTo=Networking로 주석된 receiver block), 해당 핸들러인 NetworkConnectionToWebProcess::setCORSDisablingPatterns와 그 선언부, WebPage::synchronizeCORSDisablingPatternsWithNetworkProcess()와 이를 호출하던 세 지점(WebPage 생성자, WebPage::~WebPage, WebPage::updateCORSDisablingPatterns), 그리고 WebProcess::ensureNetworkProcessConnection()에서 새로 생성된 각 network connection에 살아있는 모든 WebPage의 m_corsDisablingPatterns를 다시 게시하던 loop입니다.
새로 추가된 항목은 다음과 같습니다. NetworkProcess.messages.in에 SetCORSDisablingPatternsForPage(WebCore::ProcessIdentifier, WebCore::PageIdentifier, Vector<String>)가 추가되었습니다. NetworkProcess::setCORSDisablingPatterns(NetworkConnectionToWebProcess&, ...)는 setCORSDisablingPatternsForPage(ProcessIdentifier, PageIdentifier, Vector<String>&&)로 이름이 바뀌었고, 이제 dispatch 메커니즘으로부터 connection을 전달받는 대신 webProcessConnection(webProcessIdentifier) — null을 허용하는 RefPtr — 를 통해 스스로 connection을 조회합니다. WebPageProxy::sendCORSDisablingPatternsToNetworkProcessIfNecessary()가 새로 추가되어 WebPageProxy::setCORSDisablingPatterns()와 WebPageProxy::finishAttachingToWebProcess() 양쪽에서 호출됩니다. 후자는 process swap 상황을 처리하는 지점으로, legacyMainFrameProcess().coreProcessIdentifier()와 webPageIDInMainFrameProcess()를 전달합니다.
launch 이전 상황을 위해서는, NetworkProcessConnectionParameters에 HashMap<WebCore::PageIdentifier, Vector<String>> corsDisablingPatternsPerPage 필드가 추가되었습니다(헤더와 .serialization.in 양쪽). 이 필드는 NetworkProcessProxy::getNetworkProcessConnection()에서 page->corsDisablingPatterns()를 통해 채워지며, NetworkProcess::createNetworkConnectionToWebProcess()에서 적용됩니다.
보안 정책이 그 정책의 적용 대상이 되는 컴포넌트를 거쳐 시행 지점까지 전달되는 구조입니다. 그 컴포넌트가 침해되면 정책 자체가 위조될 수 있습니다.
Background
이 코드가 있는 위치. WebKit의 IPC 계층은 UIProcess(신뢰되는 애플리케이션 프로세스), 하나 이상의 sandboxed WebContent process, 그리고 NetworkProcess 사이에서 메시지를 전달합니다. WebContent는 renderer 버그가 exploit되는 순간 완전히 attacker 통제 하에 놓인다고 간주됩니다. UIProcess는 신뢰되는 프로세스이며, NetworkProcess는 cookie jar와 credential을 보유한 채 실제 network I/O를 수행합니다.
메시지 선언과 annotation. IPC endpoint는 .messages.in 파일에 선언됩니다. receiver block에는 DispatchedFrom=WebContent, DispatchedTo=Networking과 같은 annotation이 붙을 수 있으며, 이는 해당 block의 메시지를 어느 프로세스가 보낼 수 있는지를 명시합니다. MESSAGE_CHECK 매크로는 receiver에서 IPC 인자를 검증하고, 위반 시 발신자의 connection을 종료시키는 WebKit의 관례입니다.
Connection 객체. NetworkConnectionToWebProcess는 하나의 WebContent process connection을 나타내는 NetworkProcess 측 객체이며, NetworkProcess::webProcessConnection(WebCore::ProcessIdentifier)가 이를 조회합니다. NetworkProcessConnectionParameters는 UIProcess가 NetworkProcessProxy::getNetworkProcessConnection()을 통해 채운 뒤, WebProcess의 network connection이 수립될 때 NetworkProcess::createNetworkConnectionToWebProcess()로 전달하는 구조체입니다.
CORS-disabling patterns. 이는 페이지별 embedder 설정으로, _corsDisablingPatterns로 노출되며 WebPageProxy::setCORSDisablingPatterns를 통해 WebKit에 도달합니다. 각 문자열은 WebCore::UserContentURLPattern으로 파싱되는데, 이때 isValid()는 해당 문자열이 형식상 올바른 pattern인지만 보고합니다 — *://*/*는 모든 scheme, host, path에 매치됩니다. network 측에서는 파싱된 pattern이 NetworkProcess::shouldDisableCORSForRequestTo(PageIdentifier, URL)에서 참조되며 connection의 NetworkOriginAccessPatterns에 등록됩니다. WebCore 측에서는 동등한 목록이 Page::setCORSDisablingPatterns()를 통해 설치됩니다.
식별자. WebCore::PageIdentifier는 WebContent process 내부의 페이지를 지칭하고, WebCore::ProcessIdentifier는 WebContent process 자체를 지칭합니다. 둘 다 IPC payload에 담기는 단순한 ObjectIdentifier 정수입니다.
Process swap. 페이지는 navigation 과정에서 다른 WebContent process로 옮겨질 수 있습니다. WebPageProxy::finishAttachingToWebProcess()는 페이지가 (새로 바뀌었을 수도 있는) WebProcess에 attach된 이후 UIProcess에서 실행되며, 페이지별 상태가 다시 수립되는 지점입니다.
Analysis
pattern 목록은 원래 UIProcess에서 신뢰되는 embedder 설정으로 만들어지지만, NetworkProcess에는 오직 renderer를 경유해서만 도달했습니다.
Before: After:
UIProcess UIProcess
└─► WebPage::UpdateCORSDisablingPatterns ├─► WebPage::UpdateCORSDisablingPatterns
│ (WebContent) │ (WebContent, in-process checks only)
└─► synchronizeCORSDisabling...() └─► NetworkProcess::SetCORS...ForPage
└─► NetworkConnectionToWebProcess │
::SetCORSDisablingPatterns ◄── forgeable └─► enforcement state
└─► enforcement state
핸들러인 NetworkConnectionToWebProcess::setCORSDisablingPatterns는 인자를 아무런 MESSAGE_CHECK 없이 그대로 NetworkProcess::setCORSDisablingPatterns로 전달했습니다. pattern 문자열에 대해서도 (참조된 것은 UserContentURLPattern::isValid()뿐이었고, 이는 authorization check가 아니라 syntax check에 불과합니다), 그리고 전달된 PageIdentifier가 해당 connection이 호스팅하는 페이지에 속하는지에 대해서도 검증이 이루어지지 않았습니다. 여기서 빠져 있던 invariant는, NetworkProcess가 CORS 시행을 건너뛸지 여부를 좌우하는 값이라면 최소한 시행 지점만큼 privilege가 높은 프로세스에 의해 attest되어야 한다는 원칙입니다. 이 값을 sandboxed renderer를 거쳐 전달한 것은, renderer를 제약하기 위해 존재하는 정책에 대해 오히려 renderer의 주장을 권위 있는 것으로 만든 셈입니다.
이 취약점은 web content에서 직접 도달할 수 있는 경로는 아닙니다. endpoint가 IPC message이기 때문에, attacker는 먼저 별도의 renderer 버그를 통해 WebContent process 내에서 arbitrary code execution(최소한 arbitrary IPC-send 능력)을 확보해야 합니다. 이 조건이 갖춰지면, trigger 자체는 기존 network connection 위에서 이루어지는 단 한 번의 미인증 write입니다. SetCORSDisablingPatterns(pageID, { "*://*/*" }).
이 문자열은 형식상 올바르므로 isValid()가 true를 반환하고, connection.originAccessPatterns().allowAccessTo(parsedPattern)을 통해 pattern이 등록됩니다. 이후 shouldDisableCORSForRequestTo(pageIdentifier, url)에서 참조되기 위해 유지되는데, 해당 유지 코드는 제시된 hunk의 끝 부분 바로 다음에 위치합니다. 다만 바로 위에 있는 page 기반 signature를 보면 이 조회가 페이지 단위로 이루어짐을 알 수 있습니다. 이후 attacker는 임의의 origin에 대해 통상적인 credentialed load(fetch(url, {credentials:'include'}), XMLHttpRequest, 혹은 직접 구성한 ScheduleResourceLoad IPC)를 발생시킬 수 있으며, network 측 cross-origin check가 생략되어 응답 본문이 침해된 renderer로 그대로 반환됩니다. renderer 측 WebCore check는 아무런 장애물이 되지 못합니다. 이미 침해된 프로세스는 자신의 address space를 통제하며, UpdateCORSDisablingPatterns 핸들러 역시 local하게 보유하고 있기 때문입니다.
이 primitive를 더 날카롭게 만드는 두 가지 특성이 있습니다. 먼저 pageIdentifier 인자가 발신 connection이 호스팅하는 페이지에 속하는지 전혀 검증되지 않았기 때문에, attacker는 자신의 것이 아닌 다른 식별자를 대상으로 삼을 수도 있었습니다. 다만 NetworkProcess가 해당 엔트리를 connection 단위가 아니라 순수하게 PageIdentifier 기준으로만 키잉하는지는 제공된 context만으로는 확인되지 않습니다. 그리고 WebProcess::ensureNetworkProcessConnection()에서 삭제된 loop는 살아있는 각 WebPage의 m_corsDisablingPatterns를 새로 생성되는 모든 network connection에 자동으로 다시 게시했습니다. 따라서 attacker가 자신의 address space 안에서 해당 멤버를 함께 덮어썼다면, 위조된 메시지를 다시 보낼 필요 없이 connection teardown 시점마다 permissive한 값이 자동으로 재적용되었을 것입니다.
이번 변경은 새로운 IPC entry point인 Messages::NetworkProcess::SetCORSDisablingPatternsForPage와, NetworkProcessConnectionParameters에 새로 직렬화되는 corsDisablingPatternsPerPage 필드를 추가함으로써 NetworkProcess의 attack surface를 확장시킵니다. 이제 NetworkProcess의 CORS-disabling 상태는 NetworkProcess receiver 위의 메시지를 통해서도, connection 생성 시점에도 기록될 수 있습니다. 그리고 해당 핸들러는 dispatching connection을 사용하는 대신 메시지 안에 담긴 식별자를 이용해 connection을 조회합니다.
여기에는 강제되지 않고 가정에만 의존하는 지점이 두 곳 있습니다. 하나는 NetworkProcess receiver block이 오직 parent connection 위에서만 dispatch 가능하다는 가정입니다. diff에 보이는 block header인 messages -> NetworkProcess : AuxiliaryProcess WantsAsyncDispatchMessage {에는 제시된 발췌 범위 안에서 DispatchedFrom= annotation이 붙어 있지 않으므로, 이는 WebKit의 일반적인 process-model 기대에 의존하는 상태입니다. 다른 하나는 webProcessIdentifier 인자가 정직하다는 가정으로, 핸들러가 이를 발신 connection과 대조 검증하지 않습니다. NetworkProcessConnectionParameters 역시 UIProcess에서 작성되므로 동일한 신뢰 가정이 적용됩니다.
이 가정 중 하나라도 깨진다면, 패치 이전의 bypass가 새로운 형태로 재현될 수 있습니다. 게다가 이 경우에는 attacker가 어느 connection의 originAccessPatterns에 permissive entry가 적용될지까지 선택할 수 있다는 점이 추가됩니다. 새롭게 도달 가능해진 상태 조합도 있습니다. webProcessConnection()이 null을 반환하는 동안에도 setCORSDisablingPatternsForPage가 실행되어 파싱된 pattern이 유지되는 반면, connection 단위 pattern set은 갱신되지 않는 상태입니다. 이는 connection이 dispatch 메커니즘에 의해 공급되던 기존 구조에서는 발생할 수 없었던 조합입니다.
이 취약점은 WebContent process의 sandbox 경계, 그리고 이를 통해 network 계층에서 시행되는 same-origin policy를 약화시킵니다. 보안 모델은 침해된 renderer가 더 높은 privilege를 가진 프로세스가 보유한 정책 상태에 의해 제약된다는 전제를 두고 있습니다. 패치 이전에는 이 전제가 성립하지 않았고, renderer code execution을 확보한 attacker는 임의 URL에 대해 CORS를 비활성화하여 credentialed cross-origin 응답의 본문을 읽어낼 수 있었습니다. 이는 별도의 sandbox escape 없이도 사용자가 인증되어 있는 모든 사이트(webmail, banking, internal corporate app)에 대한 universal read에 해당합니다.
삭제된 helper 함수에는 스스로의 문제를 예고하는 주석이 남아 있었습니다. // FIXME: We should probably have this mechanism done between UIProcess and NetworkProcess directly. 즉 이 안전하지 않은 라우팅 방식은, 경계 위반으로 인식되기 훨씬 전부터 이미 suboptimal한 구조로 문서화되어 있었던 셈입니다.
이 사례가 보여주는 일반적인 패턴은 policy laundering입니다. 원래 출처에서는 신뢰할 수 있던 값이, 더 낮은 privilege를 가진 프로세스를 거쳐 전달되는 순간 신뢰할 수 없는 값으로 바뀝니다. 그런데도 이를 수신하는 코드의 mental model은 여전히 이 값을 embedder 설정처럼 취급합니다. attacker input이 아니라 설정값처럼 보이기 때문에, 이 값에 대한 MESSAGE_CHECK는 애초에 작성된 적이 없습니다. 그리고 사실 작성될 수도 없었습니다. "이 pattern이 authorized된 것인가?"라는 질문은, 신뢰되는 프로세스로부터의 attestation 없이는 network 계층에서 답할 수 없는 질문이기 때문입니다. 결국 유일하게 올바른 해법은 라우팅 자체를 바꾸는 것뿐입니다.
한 가지 짚어둘 점은, NetworkConnectionToWebProcess.messages.in에는 동일한 DispatchedFrom=WebContent block 안에 origin-policy를 변경하는 성격을 가진 이웃 항목이 최소 하나 더 남아 있다는 사실입니다. 삭제된 라인 바로 위에 위치한 RegisterURLSchemesAsCORSEnabled가 그것입니다.
Audit directions
-
자신을 제약하는 프로세스를 거쳐 전달되는 보안 정책. 시행 지점은 embedder 설정과 위조된 renderer input을 구분할 방법이 없습니다. 어떤 인자 검증으로도 이 문제는 해결되지 않는데, authorization은 애초에 payload 자체의 속성이 아니기 때문입니다. 좁게 보면,
Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in의DispatchedFrom=WebContent, DispatchedTo=Networkingblock을 읽고, load를 수행하는 대신 origin이나 scheme 정책을 변경하는 모든 메시지를 하나씩 점검해야 합니다. 삭제된 항목 바로 위에 있는RegisterURLSchemesAsCORSEnabled가 가장 가까운 이웃이며, 같은 파일 안의 origin-access-allow-list 메시지들이 그다음 후보입니다. 각각에 대해, 그 값이 원래 UIProcess의WebPageProxy/WebProcessPoolAPI에 의해 설정되었는지를 추적해야 합니다. 넓게 보면, 같은 형태의 문제는 sandboxed tier가 상위로부터 받은 상태를 다시 게시하는 모든 지점에서 나타날 수 있습니다.GPUConnectionToWebProcess.messages.in과WebProcess.messages.in의 핸들러들을 살펴보고, WebContent process가 세 번째 프로세스로 다시 전달하는 setting, sandbox extension, content-rule-list 상태가 있는지, 그리고 UIProcess 소유의 preference가 WebProcess 측에서 채워지는 connection parameter를 통해서만 NetworkProcess에 도달하는 경우가 있는지 확인할 필요가 있습니다. 가장 넓게 보면, 여기서 성립하는 invariant는 정책 결정은 그 시행 지점만큼 혹은 그보다 높은 privilege를 가진 컴포넌트에 의해 attest되어야 한다는 원칙입니다. 이 원칙은 Chromium의 Mojo 기반 browser/renderer/network-service 분리 구조, 상류에서 발급된 authorization claim을 relay하는 microservice mesh의 edge service, 그리고 OAuth 스타일의 token relaying에도 동일하게 적용됩니다. 코드 리뷰에서 잡아낼 수 있는 신호는, 낮은 privilege 프로세스 안에서 어떤 값의 유일한 writer가 inbound message를 그대로 outbound message로 복사하는 핸들러뿐이고 그 사이에 아무런 연산도 없는 경우입니다. 그 memcpy 형태의 relay가 바로 지문입니다. -
IPC 핸들러에서 식별자 자체가 authority가 되는 패턴.
Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp에서WebCore::PageIdentifier,WebCore::FrameIdentifier, 혹은WebPageProxyIdentifier인자를 받아, 이 connection이 호스팅하는 페이지에 속하는지에 대한MESSAGE_CHECK없이 곧바로 테이블 key로 사용하는 핸들러를 검색할 필요가 있습니다. 삭제된setCORSDisablingPatterns는 검증되지 않은pageIdentifier로 보안에 민감한 상태를 키잉했기 때문에, 침해된 프로세스가 다른 페이지의 entry를 대상으로 삼을 수 있었습니다. 넓게 보면, resource를 지칭하는 opaque integer를 받아, 호출자가 그 resource를 실제로 소유하는지 확인하지 않은 채 해당 resource에 대한 operation을 허용하는 모든 RPC 핸들러가 같은 위험을 갖습니다.NetworkStorageManager,WebSWServerConnection, 그리고NetworkProcess.cpp안의 page-keyed map들에서 동일한 형태를 찾아볼 필요가 있습니다. 가장 넓게 보면, 이는 ambient authority와 capability를 구분하는 문제이며, 위조 가능하지만 추측은 어려운 handle로 객체를 지칭하는 모든 RPC나 syscall 표면에 적용됩니다. Mojo interface receiver, 커스텀 프로토콜에서 번호로 전달되는 POSIX file descriptor, 멀티테넌트 HTTP API의 tenant ID 등이 그 예입니다. 매칭 신호는, 핸들러가 호출자가 제공한 key로 map을 조회한 뒤, 그 결과의 소유자를 호출자의 identity와 한 번도 비교하지 않는 패턴입니다. -
State propagation가 재배치된 이후 남은 비대칭 set/clear 쌍. NetworkProcess 안의 페이지별 CORS-disabling state가 페이지 소멸 시점에 실제로 정리되는지 확인해야 합니다. 패치 이전 설계는 이 부분을 대칭적으로 처리했습니다.
WebPage::~WebPage()가m_corsDisablingPatterns를 비우고 재동기화를 수행했고, 이 clearing 경로는 WebProcess 쪽 나머지 코드와 함께 삭제되었습니다. 반면 새로 도입된sendCORSDisablingPatternsToNetworkProcessIfNecessary()는 pattern list가 비어 있으면 조기 반환하기 때문에, clearing 목적의 update는 아예 전송되지 않습니다.NetworkConnectionToWebProcess의 페이지 teardown 처리 로직이 해당 entry를 제거하는지 추적할 필요가 있습니다. 다만 관련 handler의 본문은 제공된 context에 포함되어 있지 않습니다. 재사용된PageIdentifier가 이 state를 그대로 물려받을 가능성이 있는지도 함께 확인해야 합니다. 조금 더 넓혀 보면, refactor로 writer가 새로운 process로 옮겨질 때 그에 대응하는 eraser는 누락되기 쉬운 패턴입니다.WebPageProxy::finishAttachingToWebProcess()에서 replay되는 다른 state들도 같은 "clear 없는 set" 형태인지 점검할 필요가 있습니다. 가장 넓은 관점에서 보면, cross-process state를 publish하는 쪽은 모두 create edge와 같은 채널 위에 teardown edge를 가지고 있어야 합니다. connection ID를 key로 하는 session store, GPU resource table, Kubernetes finalizer 등이 같은 원리에 해당합니다. 코드 리뷰에서 주목할 만한 신호는if (state.isEmpty()) return;형태로 가드된 sender입니다. Empty case는 사실상 clearing case와 동일하기 때문에, publisher에 걸린 empty-guard는 거의 항상 teardown 누락을 의미합니다. -
Trust boundary refactor가 기능 커버리지를 조용히 좁히는 경우. Site isolation 환경에서, 새로 도입된 UIProcess 경로가 한 페이지에 로드될 수 있는 모든 process를 커버하는지 살펴볼 필요가 있습니다.
sendCORSDisablingPatternsToNetworkProcessIfNecessary()는legacyMainFrameProcess().coreProcessIdentifier()와webPageIDInMainFrameProcess()만을 전송 대상으로 삼습니다. 또한NetworkProcessProxy::getNetworkProcessConnection()은webProcessProxy.mainPages()만을 기준으로corsDisablingPatternsPerPage를 채웁니다. 결과적으로 cross-origin subframe process는 자신만의NetworkConnectionToWebProcess를 갖게 되는데, 이 process의NetworkOriginAccessPatterns에는 이전 방식의 per-WebPage 동기화가 만들어냈을 entry가 채워지지 않을 가능성이 있습니다. 패치 전후로 어떤 connection들의originAccessPatterns()가 채워지는지 비교하고, null-connection branch에서 parsed pattern이 보존되는 동안 connection 단위의 pattern set은 비어 있는 상태로 남는지도 확인해야 합니다. 조금 더 넓혀 보면, fan-out publisher가 단일 대상 publisher로 교체될 때는 기존에 존재하던 대상들을 모두 나열해 볼 필요가 있습니다. Site isolation 하의finishAttachingToWebProcess에 있는 다른 페이지별 state에도 같은 질문이 적용됩니다. 매칭 신호는, 페이지가 여러 process에 걸쳐 있을 수 있는 코드베이스에서 N개의 sender로 이루어진 loop가 단일 'main' process 하나만을 지정하는 send로 교체된 fix입니다.