Your development machine is running more servers than you think. A dev server, a database, a docs preview, a design tool's local bridge, a language server, a container runtime's API, an AI coding assistant's local endpoint, and whatever the last tool you installed decided to listen on.
Most of them have no authentication, because they are on localhost and localhost feels private. It is not as private as it feels, and the machine they run on holds your source code, your cloud credentials and your SSH keys.
This post is why local services are reachable and what to do about it. For anyone whose laptop can deploy to production, which is most engineers.
Any web page you visit can talk to localhost
A page in your browser can make requests to http://localhost:PORT. The same-origin policy governs whether the page can read the response, not whether the request is sent.
For a service with no authentication, sending the request is often enough. A state-changing endpoint reached by a simple request performs the action, whether or not the attacker's page can read what came back. And if the local service permits cross-origin reads, which many do to make development easy, the page reads the response too.
So the threat model for a local service is not "someone on my network". It is "any page anyone on this machine opens", which includes an advert on a documentation site.
DNS rebinding defeats the origin check
The standard mitigation is to check the Origin header and reject unknown ones. That is correct and it is not sufficient on its own.
DNS rebinding works around it: the attacker's domain resolves to their server first, then to 127.0.0.1 a moment later. The browser treats the page's origin as unchanged because the hostname has not changed, so requests to that hostname now reach your local service and the page can read the responses.
The defence that actually holds is to validate the Host header and reject anything that is not localhost or 127.0.0.1, because a rebinding attack must send the attacker's hostname. Origin checks plus Host validation together cover both cases.
Bind to the loopback interface, not to everything
A service bound to 0.0.0.0 listens on every interface, including your wireless one, which makes it reachable by anyone on the café network.
# What is listening, and on which interface?
lsof -nP -iTCP -sTCP:LISTEN | awk '{print $1, $9}' | sort -u
# or
ss -lntp
Read that list. The entries to look at are the ones showing *:PORT or 0.0.0.0:PORT rather than 127.0.0.1:PORT. Many development servers default to all interfaces because it makes testing from a phone easier, and nobody changes it back.
What is actually sitting there
Worth naming, because the risk varies enormously:
- The container runtime's API. On a Unix socket it is protected by file permissions. Exposed over TCP without TLS, it is remote code execution as root, by design, for anyone who can reach it.
- Databases started for development, frequently with no password because that is the quickstart.
- Development servers with debug endpoints, hot reload, and sometimes an eval endpoint.
- Local bridges for desktop tools, which exist to let a web application talk to something installed locally, and are therefore designed to accept requests from web pages.
- Agent and assistant endpoints, increasingly, which can read and write files in your project.
That last category is worth specific attention because it is new and it is powerful: a local endpoint that will edit files or run commands is a serious capability to expose without authentication.
The practical hygiene
Bind to loopback by default, and make the exception deliberate when you need to test from another device.
Require a token even locally for anything that changes state or reads files. A random token in a config file the tool reads is not a burden and it removes the whole class.
Validate Host and Origin in anything you write that listens locally.
Close what you are not using. Development services accumulate across projects and keep running in background terminals for weeks.
Do not expose the container runtime over TCP. If you need remote access, use the SSH-based context rather than an open port.
Check periodically from another machine. The honest test of whether your local services are local:
# from a different device on the same network
nmap -Pn --open -p 1-10000 <your-laptop-ip> | head -20
Why the laptop is the target
This matters more than it sounds because of what the machine holds. Cloud credentials in a config file, an SSH agent with keys loaded, a token with repository write access, a session cookie for your cloud console, and a checkout of your source code.
An attacker reaching a local service is not after that service. They are after a foothold on a machine with credentials to everything else, and the local service is the cheapest way in because it has no authentication and its owner was not thinking about it as exposed.
The concession
Requiring authentication on local development tooling adds friction to the thing developers care most about, which is fast iteration, and a lot of this risk is genuinely low for someone working alone on a trusted network with a small set of well-behaved tools.
The proportionate version is short: bind to loopback, validate Host on anything you wrote, and keep the container runtime off TCP. Those three take a few minutes, cost nothing in daily use, and remove the failures with the worst outcomes. The rest is judgement about which tools you trust.
The implication
Local services get no review because they are not production, and they run on the machine with the most access to production of anything you own.
Run the listening-ports command now. The interesting question is not how many there are, but which ones you cannot name.