How to block web browser-based localhost port-scans

News surfaced last month about 30 000 websites — including eBay and many banks — performing port-scanning on localhost (the local device) when you visit their sites. The scans tried to determines what services are running on the local system. It has been speculated that this data is in turn used to detect signs of malware-infections and device-fingerprinting.

This topic pops up in the tech news every two years or so but no one is proposing solutions. Here’s how you can block localhost port scans in Firefox (and why it won’t work in other web browsers).

It’s common for many types of software to run an IP-based service on the local device. This can be anything from a remote-access program to media libraries and services for communicating with video-conferencing services and web-based software update systems.

Windows 10 has over a dozen services listening on the localhost by default. Other operating systems aren’t far behind. Any type of software you install could be adding additional services without your knowledge. Vendors aren’t always good at securing these services properly.

A recent example where this went wrong is a security flaw that gave access to the webcam on macOS through an unsecured web server installed by Zoom.

It’s important to note here that web browsers treat localhost addresses differently from other origins (domains and IP addresses). It’s treated as “a potentially trustworthy origin”. Webpages can freely communicate with services listening on it unless those services take steps to block access. It’s difficult for them to do anything about port-scanning, though. It’ll respond differently from a non-existing service, which can be determined from timing how long the request takes to fulfill.

The best protection against something like this is process isolation techniques like running your web browser in a virtual machine, or switching to a more security-constrained operating system like Qubes OS. These tools are resource-intensive and inconvenient to use, however.

The method I’ve devised relies on a Proxy Auto-Configuration (PAC) file that instructs the web browser to forward all requests addressed to localhost into a null-point instead. Specifically, all connections addressed at localhost are proxied to a non-existing proxy server on port 9. This port is dedicated to discarding unwanted traffic (as specified by the Discard Protocol.)

This method will only work in web browsers that allow you to configure program-specific proxy rules. That leaves you with Firefox and its derivatives, and Konqueror. Chromium-based and WebKit-based browsers are unsupported as they rely on the operating system’s proxy settings.

You must not apply this PAC file to your entire operating system. That will cause havoc and break many programs and functionality in unique and interesting ways. Only use it in a web browser you don’t want to have access to any services listening on the localhost. You can use a secondary web browser if you need access to services running on the localhost.

What follows is a copy of a PAC file that does the desired blocking and the instructions for how to configure Firefox to use it.

  1. Save the following PAC to a file on your device. It must be saved with a .js file extension.
// SPDX-License-Identifier: CC0-1.0

const allowed_ports = [
  // uncomment and list any exceptions:
  // 80, 3000, ...
];

// CIDR 0/8 and 127/8
const local_cidrs =
  /^(?:127(?:\.(?:\d{1,})){1,3}|0+(?:\.0+){0,3})$/;

// other notations for 0/8 and 127/8
const localhosts = [
  'localhost',
  '::',
  '::1',
  '0x00.0',
  '0177.1',
  '0x7f.1'
];

function FindProxyForURL(url, host)
{
  // check if localhost
  if (localhosts.includes(host) ||
      host.match(local_cidrs)) {

    // make exceptions for allowed ports
    let port = url.match(/^\w+:\/\/[^/]+?:(\d+)\//);
    if (port) {
      port = parseInt(port[1]);

      if (allowed_ports.includes(port)) {
        return 'DIRECT';
      }
    }

    // reject by proxying to local discard
    return 'PROXY 127.0.0.1:9';
  }

  // all other requests are allowed
  return 'DIRECT';
}
  1. Drag that file into Firefox. Write down the contents of the address field. (file://…)
  2. Type about:config into the address field and press Enter.
  3. Search for network.proxy..
  4. Locate and apply the following changes to the configuration:
  • Set network.proxy.allow_hijacking_localhost to true.
  • Set network.proxy.autoconfig_url to the address you noted in step 2.
  • Set network.proxy.type to 2.

—and you’re done! Any new connections will be filtered through the PAC file. You don’t need to restart Firefox.

The above PAC file includes a couple of other addresses and notations that should cover quite a few cases. This PAC file still leaves one avenue of attack open, however. DNS domain names can be resolved to point at a localhost address. This has not been observed in any of the recent localhost port-scanning incidents, however.

You can protect against that type of address by resolving the domain name and testing against its destination. Applying this protection will introduce a significant performance penalty of 10–500 milliseconds per URL (even to the same host) that is loaded in your web browser. Firefox’s PAC parser doesn’t use Firefox’s DNS cache so the slow-down even applies to URLs under the same host. An average webpage loads 75 URLs, according to the April 2020 numbers from the HTTP Archive.

If you consider that an acceptable performance penalty for the additional protection given, you can modify the above host checking to include the following conditional:

isInNet(host, '127.0.0.1', '255.0.0.0')

Browsers won’t relax their default security protections for any addresses other than ::1 and 127.0.0.1. I included a few different notations — and the common “localhost” hostname — as a preemptive measure in case someone clever discovers a way around it in the future.

I’d like to close with a note for the history books. The Microsoft Edge web browser — the old EdgeHTML version — used to block all access to localhost by default. You could turn off the blocking but it would warn you that doing so would make your device less secure. The new Edge built on Chromium no longer blocks webpages from accessing localhost.

Maybe all web browsers should block access to localhost by default? It’s incredibly difficult for people to protect their devices against port-scanning or more targeted attacks that communicate directly with local services. Installing a magical incantation of a PAC-file isn’t an adequate solution for the masses.

Oh, and you shouldn’t install random PAC files you find on the web! Read up below to learn more about them and to understand how the above PAC file works!