← All reports

Local Network Access stops trusting the web process's client description

A compromised web process could talk its way onto the local network, no prompt shown.

Component: WebKit NetworkProcess | 87b3951

Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp

+void NetworkResourceLoader::recordLocalNetworkAccessFrame(NetworkConnectionToWebProcess& connection, const ResourceResponse& response)
+{
+ if (!isMainResource() || !connection.localNetworkAccessEnabled() || !(response.url().protocolIsInHTTPFamily() || response.url().protocolIsFile()))
+ return;
+ connection.recordLocalNetworkAccessFrame(m_parameters.webFrameID, { response.ipAddressSpace(), SecurityOriginData::fromURL(response.url()) });
+}
...
+ // A web process can label any load as a main frame load, so only trust the label for a load that started at its own validated first party.
+ if (isMainFrameLoad() && RegistrableDomain { originalRequest().url() } == RegistrableDomain { originalRequest().firstPartyForCookies() })
+ return completionHandler(std::nullopt);
...
+ auto record = clientFrameID ? connection->localNetworkAccessFrameRecord(*clientFrameID, clientParentFrameID) : std::nullopt;
+ bool recordMatchesClient = record && record->origin == sourceOrigin;
+ if (!recordMatchesClient)
+ sourceOrigin = SecurityOriginData::createOpaque();
+ auto recordedAddressSpace = recordMatchesClient && record->addressSpace != IPAddressSpace::Unknown ? record->addressSpace : IPAddressSpace::Public;
+ if (isLessPublicThan(clientAddressSpace, recordedAddressSpace))
+ clientAddressSpace = recordedAddressSpace;

Local Network Access (LNA) puts requests from more public address spaces to more private ones (local, loopback) behind a permission prompt, with exemptions for same-origin and top-level loads. The facts about the initiating document used to be serialized into NetworkResourceLoadParameters by the web process, so the check was a policy decision made on input the attacker controlled.

The NetworkProcess now records the address space and origin of the response that created each frame's document and checks the web process's claims against that record. A claim can only make a request stricter, and a mismatched origin becomes opaque. The UI-process first-party check gates the secure-context flag, and only a load that started at its own first party keeps the top-level exemption. Shared and service workers have no frame record and are refused for now. Permissions-policy state is still supplied by the web process (bug 326009).

A compromised web process could previously claim loopback, claim the target's own origin, or label a subresource as a main-frame load to reach the local network without a prompt.

Bugs in a mechanism like this tend to sit at its edges. Check record lifecycle in recordLocalNetworkAccessFrame and browsingContextRemoved: stale records, frame ID reuse, and records written under the wrong connection. Find main-resource paths that never write a record, such as redirects or service-worker-intercepted loads, beyond the three known writers (continueDidReceiveResponseAfterLocalNetworkAccessCheck, continueDidRetrieveCacheEntryAfterLocalNetworkAccessCheck, transferToNewWebProcess). Check whether the parent-frame fallback in localNetworkAccessFrameRecord lets about:blank, srcdoc, blob:, data: or sandboxed documents be judged by their parent's record. Check what a forged navigationRequester->frameID selects, the comparison semantics of SecurityOriginData, what firstPartyForCookies a web process can influence, and how redirects interact with originalRequest(). Missing records fail closed, so the real question is whether a stale but matching record can fail open.