Local Network Access: Why Your HTTPS App Suddenly Needs Permission to Reach localhost
A practical guide to Local Network Access: Chrome's LAN permission, localhost and private-IP requests, targetAddressSpace, iframes, WebSockets, mixed content and migration from PNA.
A public web app calling http://localhost:3000, a printer on 192.168.1.40, or a service discovered as device.local used to look like an ordinary networking problem.
In current Chrome, it is also a browser-permission problem.
Chrome 142 shipped Local Network Access (LNA) restrictions for requests from public sites to local and loopback addresses. The work replaces the earlier Private Network Access (PNA) direction: instead of relying on a special CORS preflight as the main gate, the browser asks the user whether the site may reach their local network.
That change matters to more than device-management software. Developer tools, desktop companions, local AI runtimes, hardware bridges, enterprise web apps and browser-based installers often use exactly this architecture:
https://app.example.com
|
| fetch / WebSocket
v
http://127.0.0.1:4317
The tempting diagnosis when this breaks is “CORS”. Sometimes CORS is still part of the request. But adding another Access-Control-Allow-Origin header cannot grant a browser permission the user has denied.
The useful mental model is address-space escalation
LNA divides network destinations into three address spaces:
| Address space | Typical destination | Meaning |
|---|---|---|
public |
203.0.113.x, public websites |
Globally reachable destination |
local |
10.x, 172.16/12, 192.168/16, link-local and .local destinations |
Meaningful on the current network |
loopback |
127.0.0.1, ::1, localhost services |
Meaningful on the current device |
The exact classification contains more ranges than the familiar RFC 1918 blocks, so do not implement your own “starts with 192.168” test and assume it matches the browser.
The security concern is a request moving from a more public context toward a less public destination. A malicious public page should not get an unlimited opportunity to probe or send state-changing requests to routers, printers, admin panels and local agents merely because the victim’s browser can reach them.
This is why CORS was never a complete answer. A classic CSRF-style attack may only need to send a request. It does not necessarily need JavaScript to read the response.
Chrome’s shipped behaviour is narrower than the abstract model
There is an important implementation detail in the current LNA specification: Chromium presently focuses enforcement on requests from the public address space to local or loopback destinations. The draft notes that it does not currently enforce the permission for cross-origin requests originating from local addresses.
That distinction matters when testing an intranet.
These two deployments are not equivalent:
https://cloud.example.com -> http://192.168.1.40
and:
http://intranet.local -> http://192.168.1.40
Do not infer production behaviour from a test page hosted on the same LAN as the device. Test from the same address-space relationship your real users have.
The permission is separate from CORS
Suppose your SaaS dashboard needs a local companion process:
const response = await fetch('http://127.0.0.1:4317/status');
There are several independent questions:
- Is the initiating page a secure context?
- Does Local Network Access permit the connection?
- Does mixed-content handling permit the HTTP target?
- If the request is cross-origin and JavaScript wants the response, does CORS permit it?
- Is the local service actually listening and accepting the request?
LNA does not replace CORS, and CORS does not replace LNA.
This separation is useful during debugging. If the user denies local-network permission, changing the local server’s CORS configuration is the wrong layer. If LNA succeeds but fetch() still reports a CORS failure, the permission prompt was not the whole problem.
Secure context on the public side is non-negotiable
The LNA permission is restricted to secure contexts.
For a production web application, that means the page asking to reach the local network should be served over HTTPS. An insecure public origin cannot use the permission as an escape hatch.
This produces an architecture that can look odd at first:
HTTPS public application
|
| user grants LNA
v
HTTP local device
Normally an HTTPS page fetching HTTP is mixed content. LNA deliberately has machinery to relax that check for permitted local destinations because many LAN devices and local companion processes cannot realistically obtain publicly trusted certificates.
The relaxation is scoped to local-network requests; it is not a general “let my HTTPS page fetch arbitrary HTTP” switch.
targetAddressSpace is a declaration, not a permission bypass
A particularly easy detail to misunderstand is targetAddressSpace on fetch() / Request.
Consider a local service exposed through a hostname:
http://agent.example.test:4317
If the hostname itself looks public but DNS resolves it to a local address, the browser cannot necessarily exempt the request from mixed-content blocking early enough. The application can declare its intent:
const response = await fetch('http://agent.example.test:4317/status', {
targetAddressSpace: 'loopback',
});
For a LAN destination the value is local:
await fetch('http://device.example.test/status', {
targetAddressSpace: 'local',
});
This does not mean “please treat any HTTP server as local”. The destination still has to resolve into the declared address space, and the relevant permission still has to be granted.
I would use targetAddressSpace when the URL does not itself make the local destination obvious, not spray it onto every fetch in the application.
Private IP literals such as 192.168.0.1 and .local names are cases where supporting browsers can already know the intended address space before resolution, so the explicit option is often unnecessary for mixed-content handling.
local and loopback are now deliberately different permissions
Earlier versions of this work used the broader name local-network-access. The current draft defines two finer-grained policy-controlled features:
local-networkfor LAN destinations;loopback-networkfor services on the user’s own machine.
Chromium keeps local-network-access as a compatibility alias, but new code and policy should use the granular names.
That split is more than naming tidiness. A web IDE that talks only to a developer daemon on localhost does not necessarily need permission to scan or contact the rest of the LAN. A device-management application may need LAN access but no loopback service at all.
Narrow permission boundaries are easier to explain to users and easier to reason about in a security review.
You can inspect the state with the Permissions API in supporting browsers:
const loopback = await navigator.permissions.query({
name: 'loopback-network',
});
console.log(loopback.state); // "granted", "denied" or "prompt"
Treat that as capability detection, not as a reason to nag the user. If your product only needs local access after somebody clicks Connect to device, that user action is a much better moment to trigger the real request than page load.
Do not prompt on startup unless the product genuinely needs it
This is the product-design trap I would expect to cause the most unnecessary damage.
Imagine an admin application that can optionally discover a printer. If its bootstrap code probes 192.168.1.1 or localhost on every page load, Chrome can turn an invisible implementation detail into a permission decision before the user has any context.
A better flow is:
User chooses “Connect local device”
|
v
Explain what will be contacted and why
|
v
Attempt the local request
|
+--> permission granted -> continue
|
+--> denied -> show recovery instructions / alternate path
The permission should correspond to a feature the user recognises. This also reduces the chance that an incidental health check becomes the request that teaches the user to click Block.
Cross-origin iframes add another policy boundary
LNA is integrated with Permissions Policy. The current directives default to self.
If a third-party embedded application needs LAN access, the top-level page cannot assume the iframe will simply prompt for itself. Delegation has to be intentional.
A parent can allow a specific origin:
Permissions-Policy: local-network=(self "https://device-ui.example")
and the iframe also needs the relevant allow declaration:
<iframe
src="https://device-ui.example/connect"
allow="local-network">
</iframe>
The same pattern exists for loopback:
<iframe
src="https://editor.example/connect"
allow="loopback-network">
</iframe>
This is where a generic “deny every Permissions Policy feature” header can become an unexpected regression. If you maintain a restrictive policy, review it before shipping a local-device integration. SiteTidy’s Permissions Policy guide covers inheritance and iframe delegation in more depth.
WebSockets are part of the migration, not an escape route
A common local-companion architecture uses WebSockets instead of fetch():
const socket = new WebSocket('ws://127.0.0.1:4317/events');
Do not treat that as a way around LNA.
Chrome expanded Local Network Access restrictions to local WebSocket connections in Chrome 147. The same release cycle also extended restrictions to WebTransport and closed a Service Worker WindowClient.navigate() gap for subframes.
That is a useful signal about how to plan the migration: inventory all browser-initiated local connections, not just REST calls.
A search for fetch('http://localhost will miss WebSockets, dynamically constructed URLs, embedded frames, service workers and libraries that discover devices on your behalf.
Main-frame navigation is a different case
Chrome’s current restrictions are aimed at requests a site can initiate behind the scenes. Chrome’s release notes for the Service Worker navigation change explicitly say the LNA restriction applies when the navigated WindowClient is a subframe and that Chrome does not enforce LNA restrictions on main-frame navigations.
That difference is sensible from a user-agency perspective. Typing or navigating the top-level tab to a router is not the same capability as an arbitrary public page silently issuing requests to it.
It also means your test matrix should distinguish:
<a href="http://192.168.1.1">Open router</a>
from:
fetch('http://192.168.1.1/api/reboot', { method: 'POST' });
They are not interchangeable tests of LNA behaviour.
Do not keep implementing the old PNA preflight model
The history is unusually confusing because Local Network Access follows an earlier Chrome effort called Private Network Access.
PNA experimented with special preflights and headers for requests into less-public address spaces. Its rollout encountered compatibility problems and was paused. The current LNA draft explicitly says it builds on that work but changes the main control to a user permission.
If you find an older migration article telling you that the solution is primarily to return headers such as PNA-specific preflight metadata, check its date before changing production infrastructure.
I would base new work on the current LNA model and current browser documentation, while retaining old PNA handling only where you have a concrete compatibility reason.
This is one of those web-platform migrations where the old terminology remains highly searchable after the implementation direction has changed.
A production migration checklist
Start by finding every path from browser code to a local destination. Search source code, but also inspect network traces for device SDKs and companion libraries.
Classify each dependency as local or loopback, and record which user-facing feature needs it. Then check these points:
| Check | What to verify |
|---|---|
| Initiating origin | Production page is HTTPS / a secure context |
| Destination | Browser will classify it as the address space you expect |
| Trigger | Request happens in a user-understandable workflow, not gratuitously at startup |
| Mixed content | Use targetAddressSpace where a hostname hides the local/loopback destination |
| CORS | Local server still returns the CORS policy required by the application |
| Permissions Policy | Parent document and iframe delegation do not block the capability |
| Protocols | Include WebSockets, WebTransport, service workers and frames in the inventory |
| Denial path | Product handles denied without an infinite retry/prompt loop |
| Browser support | Unsupported browsers retain a sensible fallback |
For a local agent, I would also authenticate the application-level protocol. Browser permission answers “may this site reach local services?” It does not prove that a request arriving at your daemon is authorised to perform every operation the daemon exposes.
A loopback HTTP server with powerful unauthenticated endpoints is still a dangerous design even after LNA is working perfectly.
Test with a real public-to-local setup
Local networking features are unusually easy to test incorrectly.
A localhost-hosted frontend can have a more privileged network position than your production site. A staging site may resolve differently through corporate DNS. Enterprise browser policy can alter behaviour. Previously granted or denied permissions can make two developer machines appear to disagree.
For a useful test, reproduce the production relationship:
real HTTPS origin
-> actual local/loopback destination
-> fresh permission state
Then test at least grant, deny and subsequent-revisit behaviour. If the feature is embedded, test the real parent and iframe combination rather than opening the child URL directly.
Use DevTools to separate network, CORS and policy errors, but make the application expose a useful failure state too. “Failed to fetch” is not enough guidance for somebody trying to connect a scanner or local development agent.
What I would ship
For a cloud application that genuinely needs a local companion, I would keep the public origin on HTTPS, distinguish LAN access from loopback access, and trigger the first local connection from an explicit feature action.
I would use targetAddressSpace only where the hostname needs it, preserve correct CORS on the local server, and authenticate the local protocol independently of browser permission.
For embedded integrations, I would delegate only the relevant local-network or loopback-network feature to the exact frame that needs it.
Most importantly, I would not “fix” LNA by making the local daemon broadly reachable from the internet or by weakening unrelated browser security controls. The browser is forcing an architecture that was previously implicit to become visible. That is inconvenient, but it is also a good moment to decide exactly which site should be able to talk to which local service, and why.
