Browser Port Scanning Protection for Localhost Services
How a web page can probe localhost ports to infer which local services you run, why common workarounds fall short, and how to verify uniform loopback behavior.
BotBrowser Team
Prefer the maintained product doc?
This article has a matching page in the docs center. Use the docs for the canonical setup flow, current flags, and long-term reference.
A web page can ask your browser to open a connection to an address on your own machine, and the browser will usually try. If a service is listening on that port, the attempt succeeds or gets a protocol-level reply. If nothing is listening, the attempt is refused. A page that repeats the attempt across a set of well-known ports ends up with a pattern of open and closed ports, and that pattern says something about the software you run.
The sections below follow that inference from a single connection attempt to a stable local-environment signal, compare the common workarounds and the gap each one leaves, and then turn to BotBrowser port protection, which makes the responses uniform. They end with a check you can run on your own machine and a plain account of what the feature does not cover.
How a page maps your local services
Every machine has a loopback interface, which exists so that programs on the same host can talk to each other without leaving it. RFC 1122 reserves the whole 127.0.0.0/8 block for this purpose, and RFC 4291 defines ::1 as the IPv6 loopback address. The hostname localhost normally resolves to one or both of them. Services that are meant only for the local user, such as development servers, database engines, remote desktop helpers, and container tooling, often listen there because the loopback interface is not reachable from other machines.
The browser, however, runs on the same machine, so page code can address those same endpoints. Nothing in a web page needs to be installed or approved for this. The page issues an ordinary connection attempt, for example through a WebSocket, an image load, or a fetch call, and then looks at how that attempt resolves. Different outcomes tell the page different things:
- A service that accepts the connection and answers in a recognizable way suggests that a specific kind of software is running on that port.
- A service that accepts the connection but answers with something the page does not understand still shows that the port is open.
- A refused connection shows that nothing is listening, and the way the refusal arrives can differ from the way an accepted connection ends.
None of this reads any data from the local service. The page learns only that a port behaves one way or another, and with enough ports that is a surprisingly detailed description of a machine. A page that checks a few dozen well-known ports can often tell whether the visitor runs a web development stack, a database, a remote access tool, or a container platform.
This matters beyond tracking, because many local services are written on the assumption that only programs on the same machine can reach them. They often skip sign-in, listen on a well-known port, and expose a simple HTTP interface. A page that learns that such a service exists has gained information, and a page that can also send requests to it may gain more. Browsers have tightened cross-origin rules over the years, but knowing that a port is open takes far less than reading a response. That is why the open-versus-closed pattern itself is the thing worth protecting.
The Local Network Access proposal from the WICG describes this class of request, where a page served from a public site reaches toward local and loopback addresses, and proposes that browsers should gate such requests behind a permission. It is a community specification, and support differs between browsers and versions, so do not assume that your current browser prompts or blocks. Check what your own build does before relying on it.
Why the pattern follows you across sessions
Cookies, local storage, and an IP address are all things that you can clear or change. The set of services running on your computer is different, because it describes how you work rather than where you connect from. A developer who runs the same editor integration, the same database, and the same container runtime every day will present the same pattern on Monday and on Friday.
That stability is what makes the signal useful for tracking. Consider the common ways people try to start fresh:
- Clearing cookies and site data removes the state a site stored in your browser, but it does not change which ports are open.
- Switching to a different IP address, for example through a proxy, changes the network location the site sees, but the loopback pattern belongs to the machine and travels with it.
- Opening a private window or a second browser profile resets browser state, but the same machine answers the same way.
The signal also combines well with others. A pattern of open and closed ports is one more attribute that can be joined with canvas output, font availability, and the rest of the surface a fingerprinting script collects. Each attribute alone is partial, and together they can single out a machine, which is why one weak signal is still worth closing.
Not every machine is equally exposed. A laptop used only for email and documents may show almost no open ports, and its pattern is then close to what many other machines show. A workstation or server with a dozen services is far more distinctive. The people who gain most from handling this signal are developers, operators of headless servers, and anyone who runs remote access tools, since those setups listen on more ports than the typical consumer machine.
Why the usual workarounds fall short
Several approaches look reasonable, and each leaves a gap.
Blocking loopback connections outright. If the browser refuses every attempt to reach localhost, the page no longer learns which service is running. But the refusal itself becomes an observable behavior. A blocked attempt can look different from a normal refusal, and an identical instant failure on every port is unusual compared with the variation a normal browser shows. Blocking also breaks things you may rely on: desktop companion apps that talk to a web page, development servers with live reload, and sign-in flows that redirect back to a local listener.
Browser extensions. Request filters can block connections to local addresses, but they work in the browser's extension layer, and coverage depends on the request type. A rule that handles fetch calls may not cover every other way a page can open a connection. The extension itself can also leave traces that a page can observe, so the protection can become another distinguishing attribute.
Firewall rules. Operating system firewalls decide which programs may accept inbound connections or reach the network. A page-initiated connection to localhost is a local connection from the browser, so a firewall rule tuned for traffic from the internet does not address it. A rule that stops the browser from reaching loopback would again break the local tools you want to keep and bring back the problem of a distinctive refusal.
Network namespaces and containers. Running the browser in a container with its own network namespace stops it from seeing the host's services, which does separate the two. The cost is operational: you now maintain an extra layer, you cannot easily reach host services you do want, and the container exposes its own set of listening services that a page can probe. It is a reasonable isolation tool for some deployments and a heavy answer to this particular problem.
VPN routing. A VPN changes where outbound internet traffic goes. Loopback traffic never leaves the machine, so routing changes do not alter how the browser answers a connection attempt to 127.0.0.1. You add a network hop and some overhead and leave the actual behavior untouched.
What these options share is that none of them makes the responses uniform. They either remove the response, which is noticeable and disruptive, or they operate at a layer that does not see the connection. The goal that actually matches the problem is narrower: when a page attempts a connection to a commonly probed loopback port, the result should not depend on whether a service is listening.
Turning on port protection
BotBrowser documents port protection as a PRO feature. It is off by default, and you enable it either with a launch flag or with a profile setting. The CLI form is a single flag added to the normal launch command:
chrome --bot-profile="path/to/profile.enc" --bot-port-protection
The same switch can live in the profile JSON, which suits profiles where protection should always be on and the flag should never be forgotten:
{
"configs": {
"portProtection": true
}
}
When the setting is in the profile, it applies as soon as the profile loads, and no extra flag is needed. If you launch through an automation framework, add the same flag to the launch arguments the way you would add any other BotBrowser flag. The port protection documentation shows the same flag in a Playwright launch and in a combined proxy setup.
The documented behavior is that connection attempts from web pages to commonly probed ports behave consistently across loopback addresses. The CLI flags reference lists the covered set as 30 commonly probed ports across three address forms: the IPv4 loopback block 127.0.0.0/8, the IPv6 loopback address ::1, and the localhost hostname. The port protection guide describes the same behavior without giving a count.
Choose the flag when you want protection for a single launch, for example while you test. Choose the profile setting when a given profile should always carry it. Teams that keep launch scripts in version control often prefer the profile setting, because a protected profile stays protected no matter which script starts it, and a review of the profile file shows the intent. Keep in mind that the feature is off by default, so a profile without the setting and a launch without the flag both run unprotected.
Headless servers and shared build machines deserve a note of their own. A browser on a server that hosts databases, monitoring agents, and container tooling sits next to many local listeners, and a page it visits can see the same kind of pattern that a workstation shows. If that browser belongs to a fleet that should behave consistently, one server's distinctive set of listeners is an unwanted difference. Setting the option in the profile used by those jobs keeps the behavior the same without relying on each launch script.
Two details matter for planning. First, the protection is about attempts made by web pages. The documentation states that local tools connecting directly are not affected, so your terminal, your database client, and your editor keep working the way they did. Second, the covered set is a fixed list of commonly probed ports and not every port on the machine. If you need coverage for a port outside it, the documentation points you to support, and you should not assume it is included.
Verifying uniform results with a local test service
Verification should show two things: page-initiated results are uniform with protection on, and your own tools still work. You can check both on a machine you own with a throwaway service. Do this only against your own machine. Testing against hosts you do not control is not a verification step and is not covered here.
Start a harmless service bound to the loopback address on a port you can spare:
python3 -m http.server 8080 --bind 127.0.0.1
Then work through the checks in order:
- Run a baseline without port protection. Use a small test page that you control, loaded in the browser, which attempts a connection from page code to your throwaway service and to an unused port on the same address and records how each attempt resolves.
- Launch again with
--bot-port-protectionorconfigs.portProtectionand repeat the same page. With protection active, the result for the open port and the unused port should be indistinguishable to the page. - Repeat the attempt against the forms the documentation names: the IPv4 loopback address, the IPv6 address ::1, and the localhost hostname. The result should be uniform for each form, not only for one.
- From a terminal, connect to the throwaway service directly, for example with
curl http://127.0.0.1:8080/. It should answer normally, because direct tool connections are unaffected. - If you depend on a web page talking to a local companion app, test that integration with protection on. The documentation describes consistent behavior for page-initiated attempts to commonly probed ports, so any page-to-local-app flow on one of those ports deserves its own check before you roll the setting out.
- Record the launch arguments, the profile in use, and the results, and repeat the test after changes to the profile, the browser build, or the launch scripts.
A useful record is short: the address form, the port, whether it was the throwaway service or an unused port, and what the page observed. Keep it to your own test ports. A record that lists only outcomes for ports you opened yourself is enough to show uniformity, and it leaves no inventory of your real services in a shared document.
If the baseline run already looks uniform, the comparison tells you little, because your test page could not tell the two ports apart even without protection. If the protected run still shows a difference on a port that you expect to be covered, check that the flag or the profile setting reached the browser, and confirm the port against the documentation. A port outside the covered set behaves as before. That is a coverage question to raise with support, not necessarily a malfunction.
What BotBrowser supports and what stays outside it
BotBrowser PRO supports port protection through the --bot-port-protection flag or configs.portProtection in a profile, and it makes connection attempts from web pages to commonly probed loopback ports (30 ports across 127.0.0.0/8, ::1, and localhost) behave consistently, so a page cannot map which local services you run. BotBrowser cannot guarantee coverage of ports outside the documented 30 or of local-network ranges such as 192.168.x.x, cannot change connections made directly by local tools, and does not replace proxy, DNS, WebRTC, or other fingerprint protections.
The scope is deliberately narrow. The addresses named in the documentation are loopback addresses. Ranges used by other devices on your network, such as 192.168.x.x, 10.x.x.x, and 172.16.x.x, are not part of the documented coverage, so do not describe them as protected. The feature is also one layer among several: your IP address, DNS lookups, and WebRTC candidates are separate surfaces that need their own handling, and so do canvas, font, and similar signals.
For the network layers around this one, see DNS leak prevention, WebRTC leak prevention, and proxy, DNS, and WebRTC consistency. Recheck the port protection documentation and the CLI flags reference before changing the covered ranges, the port count, the license tier, or the default state in your own notes, because those details are the ones most likely to change between releases.
Sources
Related Articles
Take BotBrowser from research to production
The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.