v154 · security · local network access

Local Network Access restrictions for Background Fetch

Local Network Access asks the user before a page reaches a device on their network. Background Fetch was not asking — the same gap that let it skip CORS also let it reach a router or a printer with no prompt, from a service worker, after the page had closed. Chrome 154 requires the permission here too.

concepts

  1. The permission, before you ask

    Query the local-network permission through the Permissions API without triggering a prompt, and see what each state means for a background fetch that has not started yet. A service worker cannot prompt, so knowing the state in advance is the difference between a download that works and one that fails silently.

  2. Which targets are affected

    Classify a background fetch's URL list by address space and see which entries now need the permission. The awkward ones are the hostnames: they cannot be classified until they resolve, which is why the page has to declare its intent rather than the browser inferring it.

  3. Attempting one

    A real background fetch aimed at a loopback address, with the outcome reported from the service worker. What you see depends on where this page is running, and the demo says which case it is rather than staging one.

why it shipped

A background fetch is the most attractive possible vehicle for a local network probe. It outlives the page, so a user who closes the tab does not stop it. It runs in the service worker, so there is no visible page to associate it with. And it takes an arbitrary URL list. Reaching a private address that way, with no permission, is exactly the attack Local Network Access exists to prevent.

The Background Fetch spec always said its requests go through Fetch with the same policies. This is the implementation catching up — and like the CORS half of it, anything that breaks was relying on a gap rather than on a documented behaviour.

the API

// A service worker cannot show a permission prompt, so take it in the
// page first — then the background fetch inherits the grant.
const status = await navigator.permissions.query({ name: "local-network" });
if (status.state !== "granted") {
  // Ask from a user gesture, in the page, before registering the fetch.
}

const registration = await navigator.serviceWorker.ready;
await registration.backgroundFetch.fetch("firmware", [
  "http://192.168.1.20/firmware.bin",
], { title: "Updating the printer", downloadTotal: 4_000_000 });

references