Browser debugging

Localhost Is Not A Security Boundary

Published June 25, 2026

I keep seeing local developer tools treat localhost as if it means only my code can talk to this. It does not.

The short answer: bind to loopback when you mean local, but put authentication, origin checks, and command limits on the local service anyway.

The mental model

127.0.0.1, ::1, and usually localhost point back to the same machine. That is a routing property. It says where packets go. It does not say who on the machine made the request, what browser tab triggered it, or whether the request is allowed to run a command.

Browsers also treat local origins specially. MDN documents http://127.0.0.1, http://localhost, and http://*.localhost as potentially trustworthy origins. That is useful for development because local pages can use APIs that normally require a secure context. It is not a permission model for your local HTTP server.

Same-origin policy is narrower than people remember. The browser origin is the scheme, host, and port. http://localhost:3000 and http://localhost:9400 are different origins. That blocks many reads unless CORS allows them, but it does not make the second port private.

What can still happen

A random website should not be able to read arbitrary JSON from http://127.0.0.1:9400 unless your service opts into it with CORS. Good. But read access is only one case.

Cross-origin writes, embeds, redirects, form posts, image loads, and script loads have different rules. MDN's same-origin docs say cross-origin writes are typically allowed, cross-origin embeds are typically allowed, and cross-origin reads are typically blocked. That distinction matters for local tools.

If your local service has a side-effecting GET endpoint, this is enough to be a bug:

<img src="http://127.0.0.1:9400/run?command=delete-cache">

The page probably cannot read the response. The local service may still receive the request. If the server treats that request as a command, the damage already happened.

Modern browsers are adding more local-network protections. Chrome's Local Network Access work classifies loopback as local and is moving toward permission prompts for local network requests. That helps. It does not replace application checks on a tool listening on localhost, and Chrome's own current notes call out feature gaps such as WebSocket coverage while the rollout continues.

A quick diagnostic

When I review a local browser tool, I check these first.

The agent case makes this more important. A browser debugging tool is designed to do useful work from a prompt: inspect the page, click the button, fill the form, read the console, save the screenshot. If the local API accepts commands from any caller that can reach the port, the browser tab is no longer the only thing driving those actions.

The command server should reject "this came from my laptop" as proof of intent. It should check the request before it touches Chrome. That means the boring checks happen before selector resolution, before tab lookup, before file writes, and before anything that changes page state.

Check the bind address

lsof -nP -iTCP:9400 -sTCP:LISTEN

Look for 127.0.0.1 on the expected port, or the IPv6 loopback address on the expected port. Start there. 0.0.0.0:9400 means the service is listening on all IPv4 interfaces. For a dev helper, that should make you stop and ask why.

Check whether a browser page can trigger commands

curl -i http://127.0.0.1:9400/health
curl -i -X POST http://127.0.0.1:9400/ \
  -H 'Content-Type: application/json' \
  -H 'Origin: https://example.com' \
  -d '{"command":"ping","params":{}}'

I want the command endpoint to reject unexpected browser origins or require a token the website cannot know. Returning Access-Control-Allow-Origin: * from a local command API is a bad default, especially if credentials or command responses are involved.

The response I want is boring. A request with no token should get a 401 or 403. A request with an unexpected Origin should get a 403 before command parsing. The server can still answer /health, but health should not include secrets, open tabs, filesystem paths, environment values, or anything an unrelated website could use to fingerprint the developer machine.

I also check that preflight behavior matches the command behavior. If an OPTIONS request from https://example.com gets permissive CORS headers while the real POST later rejects, the browser may still block the read, but the server is sending mixed signals. Keep the policy simple: known origins and known tokens pass, everything else fails early.

Check the methods that change state

GET should be boring: health checks, static files, version metadata. Commands should use POST, validate Content-Type, parse only the shape you expect, and reject unknown commands.

I also check non-fetch paths. If the tool exposes WebSocket, Server-Sent Events, static script files, callback URLs, or redirect endpoints, test those separately. Browser protections do not all move at the same speed, and local network permission work can cover one path before another.

For browser debugging tools, I also test the boring failure path: open the endpoint from a normal tab, a private window, and a page on a different origin. The answer should be the same each time. Health metadata can answer. Commands should not.

This is easy to miss during local development because the happy path is usually a CLI call from the agent. CLI calls do not send browser Origin headers. They also do not exercise embeds, forms, or preflight requests. I test both paths because the tool has two audiences: the agent that should be allowed to call it, and every browser page that should not. The differences only show up when the request comes through the browser.

# This should not click anything, install anything, delete anything, or open a tab.
curl -i "http://127.0.0.1:9400/click?selector=button"

What I use instead

For a localhost agent bridge, I want these checks in the local service.

  • Bind to 127.0.0.1 and ::1, not every network interface.
  • Require a token for command endpoints. Put it in an environment variable or generated local config, not in a web page.
  • Reject unexpected Origin headers on browser-shaped requests. Do not use wildcard CORS on command responses.
  • Keep GET side-effect free. If an endpoint can click, fill, navigate, write a file, or run a shell command, it is not a GET endpoint.
  • Use an allowlist of commands and parameter schemas. Unknown command means reject, not pass through.
  • Log the command name, target tab, and decision. Do not log full page contents, cookies, tokens, or request bodies.

This is also why DevSnoop uses a Chrome extension plus Native Messaging for browser access. Chrome starts the native host as a registered local application and the extension talks to it through Chrome's native messaging channel. The local HTTP API is just the part agents call.

The useful rule

Treat localhost as a transport choice. It keeps traffic on the machine when the bind address is right. It does not identify the caller, authorize a command, or make browser behavior simple.

When an agent can inspect a page, click buttons, read logs, and capture screenshots, the local API needs boring checks before every command. Without those checks, any local process or browser path that can reach the port becomes part of your command surface.

Sources