URLLoaderFactory Trust Boundary
The point where the privileged browser process stamps a renderer-initiated network request with its origin lock, isolation state, cookie policy, and trust level by creating the factory the renderer must use, rather than letting the renderer choose its own request authority.
URLLoaderFactory is a Chromium class name, but the concept is simpler than the name suggests. A renderer does not get raw network access. It gets a browser-minted factory, and that factory carries the origin, isolation state, and security policy every request through it must obey.
What It Is
At runtime, a renderer process holds no sockets, DNS resolver, cookie store, or cache. When a page fetches a script, loads an image, starts a worker, or follows a navigation, the bytes move through the Network Service. The renderer enters that service through a network::mojom::URLLoaderFactory the browser handed it. The boundary this entry names is the moment the privileged browser process creates that factory: it builds a URLLoaderFactoryParams record, fills the security-sensitive fields, and passes the finished factory to the less-privileged consumer. The renderer then calls CreateLoaderAndStart, but it does not choose the request authority attached to the call.
The params carry the authority. URLLoaderFactoryParams includes the consuming process’s process_id, the request_initiator_origin_lock that pins which origin every request from this factory may claim as initiator, the isolation_info that fixes the request’s NetworkIsolationKey and SiteForCookies, an is_orb_enabled flag, a disable_web_security flag, and an is_trusted bit that decides whether the factory’s clients may set sensitive per-request fields. A factory built for a renderer has its origin lock set and is never shared across renderer processes; a factory marked is_trusted is reserved for browser-process callers that may vary those fields per request. The browser fills these values from state it already holds (the committed document’s origin, the frame’s isolation info, the process the factory is bound to), not from the renderer’s claims.
URLLoaderFactory sits inside a NetworkContext, the privileged, browser-owned object that owns a profile’s cookie store, cache, and socket pools. The browser creates each NetworkContext; less-privileged consumers receive factories from it, not the context itself. The outer factory a renderer talks to is a CorsURLLoaderFactory, which wraps the internal network loader factory in the layer that enforces CORS and Opaque Response Blocking before any cross-origin bytes reach the renderer. The boundary is therefore two things at once: a creation boundary, where browser-side state becomes request policy, and an enforcement boundary, where the Network Service applies CORS and ORB to the responses that come back.
Why It Matters
Naming the factory boundary answers a question the Browser-Renderer Privilege Split leaves open. The split says the renderer holds no network access and the browser does. It does not say how a renderer that legitimately needs to fetch a subresource actually does so without acquiring the privilege. The factory is the answer: a single, scoped capability the browser mints and hands over, with the security-sensitive decisions already made. The renderer gets to issue requests; it does not get to authorize them.
That separation is the operational form of the Untrusted Renderer Axiom for the network path. A compromised renderer would like to fetch a cross-site resource while claiming a different initiator origin, read a response the same-origin policy should withhold, or land its requests in a victim site’s cache partition. The origin lock forecloses the first: every request from a renderer’s factory is checked against the locked initiator, and a request that claims a different origin is rejected before it leaves the browser. The CORS and ORB layer forecloses the second: a cross-origin response that fails the checks is blocked before its bytes are delivered to a renderer that should not read them. The IsolationInfo baked into the factory forecloses the third: the cache and cookie partition is the browser’s choice, fixed at factory creation, not a field the request can set. None of these defenses would hold if the renderer could pick its own factory params.
The boundary also explains why one request shape is insufficient. A frame, an isolated content-script world, a dedicated worker, a service worker, a prefetch, and an early-hints preload each need different params: a different initiator lock, a different isolation key, sometimes a trusted factory and sometimes not. The browser’s factory-construction paths fill these differently for each consumer class, so there is no single global factory a renderer reuses for everything. A reviewer evaluating a new network-adjacent feature asks which consumer class it serves and whether the params match that class; the answer determines whether the design is sound.
For enterprise security review, the factory boundary turns two recurring questions into concrete objects. Who was allowed to set this request’s initiator origin, and why resolves to the request_initiator_origin_lock on the factory the request came from. Why did this cache or cookie partition apply resolves to the IsolationInfo the browser stamped at creation. Both are browser-side facts, traceable to a specific factory and the params that built it.
For downstream Chromium-based products, the boundary is inherited along with a hazard. The ContentBrowserClient interface lets an embedder intercept factory creation to add headers, redirect handling, or custom protocol support. The project documents two ways to do this, and they are not equivalent. Replacing the factory the renderer receives means the embedder is now responsible for every security check the Network Service would have run. Supplying a factory_override instead keeps the Network Service factory in the outer position, so CORS, ORB, and the origin lock still run, and the embedder’s logic layers on top. An embedder that takes the first path to save effort reintroduces the attack surface the upstream design removed, which is the shape of several downstream incidents the Supply-Chain Vulnerability Lag entry describes.
How to Recognize It
The boundary surfaces at several points in the source tree, the IPC definitions, and the running browser.
In the IPC layer, services/network/public/mojom/network_context.mojom declares URLLoaderFactoryParams and annotates the security-sensitive fields with the rule that the browser, not the renderer, must set them. The comments name request_initiator_origin_lock, isolation_info, is_orb_enabled, disable_web_security, and is_trusted as fields a renderer-bound factory may not choose, and state that a factory vended to a renderer is origin-locked and not shared across renderer processes. The .mojom file is the precise statement of which decisions live on which side of the boundary.
In the browser-side construction code, content/browser/url_loader_factory_params_helper.cc fills the params. The helper has distinct paths for frames, isolated worlds (extension content scripts), dedicated and shared workers, service workers, prefetch, and early-hints preloads. Each path sets a different origin lock and isolation info for that consumer. Reading the helper shows that the factory boundary is not one decision but a family of them, parameterized by who is asking.
In the enforcement layer, services/network/cors/cors_url_loader_factory.cc is the outer factory that wraps the internal network loader factory. It runs the CORS preflight and response checks and the ORB logic, and it restricts automatically assigned IsolationInfo to the browser process so a renderer-supplied request cannot pick its own partition. A new feature that needs cross-origin responses delivered to a renderer passes through this factory, and a reviewer checks that it does rather than bypassing it.
In a running browser, the boundary is visible in the process layout. When the Network Service runs out-of-process (the default on desktop), a separate Network Service process appears, and the factories renderers hold are Mojo pipes into it. When that process crashes, the factories disconnect, and the browser re-creates NetworkContext objects and re-vends factories, which is why a Network Service crash is recoverable rather than fatal. On Android the service currently defaults to in-process, but the request API is the same Mojo service boundary either way.
How It Plays Out
Three scenarios show the boundary in operation.
A page in a renderer fetches a cross-origin JSON document with fetch(). The renderer issues the request through the factory the browser gave its frame. The request’s initiator is checked against the frame’s request_initiator_origin_lock, so the renderer cannot forge a different origin. The Network Service performs the request, and on the way back the CorsURLLoaderFactory applies the CORS checks: if the server did not return the matching Access-Control-Allow-Origin, the response is blocked. ORB independently refuses to hand a cross-origin, non-CORS resource of a sensitive content type to the renderer at all. The renderer receives an error, not the bytes. The browser-side factory and its params, not the renderer’s request, decided the outcome.
A contributor adds a feature that lets an extension content script fetch on behalf of its host page. The naive draft reuses the host frame’s factory. Review rejects it on the boundary: an isolated world has a different initiator origin than the page it runs in, so reusing the frame’s origin-locked factory would let the content script’s requests claim the page’s origin. The revised design routes through the isolated-world path in url_loader_factory_params_helper.cc, which builds a factory locked to the extension’s origin with the isolation info the extension’s requests should carry. The factory the script receives now stamps the correct authority, and the boundary holds.
A downstream embedder adds a custom scheme and intercepts factory creation through ContentBrowserClient to handle it. The first implementation swaps the renderer’s factory for the embedder’s own, and a security audit finds that the embedder’s factory skips the origin-lock check, so any document can issue a request claiming any initiator. The fix moves the embedder to the factory_override path: the Network Service factory stays in the outer position and keeps enforcing the origin lock and the CORS and ORB checks, while the embedder’s custom-scheme logic runs as an inner layer. The post-mortem cites the upstream documentation that warns the override path preserves the Network Service’s security features and the replacement path does not.
Consequences
Holding the factory boundary as a named invariant gives the project several properties.
Request authority is decided once, on the privileged side, and is auditable. Every security-sensitive field of a request traces to the params of the factory it came from, and every factory traces to the browser-side code that built it. A question about why a request carried a given origin or landed in a given cache partition has a single answer with a single owner, rather than scattered per-request logic on the untrusted side.
The consumer-class proliferation is explicit rather than accidental. Because frames, isolated worlds, workers, prefetch, and preload each get their own factory with their own params, a new consumer must declare which class it belongs to and accept that class’s authority. A feature that wants to reuse an existing factory to avoid the work is, in effect, asking to inherit another class’s origin lock and isolation info, and review treats that as the security claim it is.
The CORS and ORB enforcement is positioned where a compromised renderer cannot remove it. Because the checks run in the outer factory inside the Network Service, not in renderer-side code, a renderer under attacker control cannot disable them. This is the network-path instance of the general rule that the load-bearing check runs on the trusted side of the boundary.
There are real costs. Every cross-boundary request pays the Mojo IPC cost of crossing into the Network Service, and out-of-process operation adds a process hop that in-process operation avoids. Maintaining a separate factory per consumer class is more code than a single shared factory would be, and the params plumbing has enough moving fields that filling it wrong is a recurring source of bugs. The project pays these costs because the alternative is letting the untrusted side choose its own network authority.
Notes for Agent Context
Treat the browser process as the sole authority for a request’s security-sensitive identity. Never construct a renderer-bound URLLoaderFactory with a request_initiator_origin_lock, isolation_info, is_orb_enabled, disable_web_security, or is_trusted value taken from a renderer message; fill those fields from browser-side state in content/browser/url_loader_factory_params_helper.cc. Do not reuse a factory built for one consumer class (frame, isolated world, worker, prefetch, preload) to serve another; each class needs its own origin lock and isolation info. When intercepting factory creation in ContentBrowserClient, use the factory_override path so the Network Service factory stays outermost and keeps enforcing CORS, ORB, and the origin lock; never replace the renderer’s factory with one that skips those checks, and set is_trusted only on factories vended to browser-process callers.
Related Articles
Sources
The canonical primary source is the Chromium project’s services/network/README.md, which describes the Network Service as a Mojo service the browser process launches, explains why out-of-process operation is preferred for isolation and stability, and notes that a service crash disconnects and re-creates factories. net/docs/life-of-a-url-request.md walks a request from the browser-owned, privileged NetworkContext through URLLoaderFactory, URLLoader, and URLRequest, and states that the browser creates factory objects with security and privacy fields that less-privileged consumers are not trusted to set. docs/security/compromised-renderers.md makes the security claim explicit: only the privileged browser process should create HTTP URLLoaderFactory objects, so it can control the origin lock, ORB behavior, disable_web_security, and isolation info, and the Network Service enforces ORB before handing responses to renderers. The services/network/public/mojom/network_context.mojom interface documents the per-field security notes on URLLoaderFactoryParams. content/browser/url_loader_factory_params_helper.cc shows the distinct browser-side construction paths for frames, isolated worlds, workers, prefetch, and early-hints preload. content/public/browser/content_browser_client.h documents the embedder override hook and the rule that the factory_override path preserves Network Service security features better than swapping the receiver. services/network/cors/cors_url_loader_factory.cc shows the CORS and ORB layer and the browser-only restriction on automatically assigned IsolationInfo.
Technical Drill-Down
services/network/README.md(pinned2dda518) — where the Network Service runs, why out-of-process is preferred, and what a service crash does to outstanding factories; the operational overview for the whole subsystem.net/docs/life-of-a-url-request.md(pinned2dda518) — the end-to-end walk fromNetworkContextthroughURLLoaderFactory,URLLoader, andURLRequestto the response body; the statement that the browser sets fields less-privileged consumers may not is in the architecture section.docs/security/compromised-renderers.md(pinned2dda518) — the security rationale for browser-only factory creation and for ORB enforcement before responses reach a renderer.services/network/public/mojom/network_context.mojom(pinned2dda518) — theURLLoaderFactoryParamsdeclaration with the per-field annotations naming which fields the renderer may not set; the origin-lock and no-sharing rules for renderer factories are in the comments here.content/browser/url_loader_factory_params_helper.cc(pinned4b137ea) — the browser-side construction paths that fill the params differently for frames, isolated worlds, workers, prefetch, and preloads.content/public/browser/content_browser_client.h(pinnedaa8939b) — theWillCreateURLLoaderFactoryoverride hook and the documentation thatfactory_overridepreserves Network Service security features that a receiver swap loses.services/network/cors/cors_url_loader_factory.cc(pinned7b19613) — the outer factory that composes CORS and ORB around the internal network loader factory and restricts automatically assignedIsolationInfoto the browser process.