Update to Tailscale v1.32.3, which was released today. Note that Tailscale does not automatically update itself. Administrators can see the current version running on all devices on the Tailscale admin page.
This mitigation can be tested with curl – if you can put something random in the Host header, and you can view information or take actions which should not be available to the public, action is required:
Alternatively, you can use Tailscale's built-in TLS certificate support to run internal services with HTTPS (and with HTTP disabled or just redirecting to HTTPS), either directly or via something like caddy.
Tailscale has published remediation advice for this issue here.
Keep using Tailscale! 💕
The speed and quality of Tailscale's response to our report is unlike any vendor interaction I have experienced, and suggests a deep commitment to keeping their customers safe.
Tailscale is a mesh VPN service: nodes on a Tailscale network establish direct Wireguard connections to one another on-demand, using information pushed out by a central control plane (what IPs each node can be found at, what Wireguard public keys they use, which nodes are allowed to access which ports, etc.).
On each node, a process called tailscaled does all the heavy lifting – talking to the control plane, setting up the TUN interface, and carrying packets back and forth. A separate process provides a tray icon and configuration GUI in the Windows taskbar (or the macOS menu bar). On Linux, configuration is performed solely through the tailscale command-linux utility. These front-end interfaces communicate with tailscaled through an HTTP API called the LocalAPI.
On unix platforms, the LocalAPI is bound to an AF_UNIX socket, with a tiered permission structure (basic read-only access for most unix users, privileged access for root or another specially designated user).
On Windows, which lacks (or lacked!) AF_UNIX, the LocalAPI instead binds to a loopback TCP socket, 127.0.0.1:41112. It checks netstat to enforce that incoming TCP connections are from the expected Windows user, emulating the unix socket privilege model described above.
In a world where our computers run only trusted code, these two approaches would provide equivalent security to one another. The world we actually live in is much, much, messier.
The Treacherous User Agent
This is a pretty bad idea, but luckily even the web browser has its limits. If my code asks, for example, to perform an HTTP request to /var/run/tailscale/tailscaled.sock, I will be laughed out of the V8 engine before I can so much as connect(3).
If my code asks to speak to 127.0.0.1:41112, the browser is a little more receptive... but only a little. The types of requests I can make are limited, and I cannot read the responses.
If, on the other hand, my code asks to speak to my very own website... what could possibly be malicious about that? The browser allows abitrary requests to be made, and responses to be read.
This last idea is known as the Same-Origin Policy, and is a critical layer of asbestos fireproofing keeping the modern web from going up in flames.
Unfortunately, the Same-Origin Policy is somewhat flawed in its interpretation of what is and is not "my website". Let's say I host my website at the memorable domain of s-184.108.40.206-127.0.0.1-12345-rr-e.d.rebind.it:
$ host s-220.127.116.11-127.0.0.1-12345-rr-e.d.rebind.it
s-18.104.22.168-127.0.0.1-12345-rr-e.d.rebind.it has address 22.214.171.124
$ host s-126.96.36.199-127.0.0.1-12345-rr-e.d.rebind.it
s-188.8.131.52-127.0.0.1-12345-rr-e.d.rebind.it has address 127.0.0.1
When my web page (which was initially loaded from a server at 184.108.40.206) wishes the make a request to its own domain, the browser will check with DNS again, just in case I've decided to move my web server down the street in the time since you loaded the page. Surprisingly, I have! My web server is now located at 127.0.0.1!
The browser dutifully carries out the request to this new IP address, without ever deeming the request to be Cross-Origin, since the domain of the webpage matches the domain of the new request. This is called a DNS Rebinding attack.
Issue 1 - LocalAPI vulnerable to DNS Rebinding on Windows
Applying this technique, our malicious website can make arbitrary requests to the LocalAPI. Since the API does not apply any additional authentication, apart from checking that our browser is running as the same Windows user as the Tailscale GUI, we have full privileges, and can introspect and reconfigure tailscaled to taste.
What will we do with our newfound abilities?
The status and whois endpoints expose details (hostnames, TS and real IP addresses, service lists) of the machines on the tailnet, as well as the names, email addresses, and profile pictures of machines' owners.
We can also learn the Wireguard private key used by the node. This is sanitised when the preferences are requested:
This key should allow us to impersonate the targeted Windows node on the tailnet, opening Wireguard connections and accessing private services that should not be exposed to us. Unfortunately, there are some additional custom packets required to talk to a Tailscale node on top of pure Wireguard. The relevant code is open source, but using it would require writing an unconscionable amount of Go. Thus, we move on...
Becoming the Control Plane
A PATCH to /localapi/v0/prefs can be used to update ControlURL, moving the machine to an attacker-controlled control plane server, away from the default ofhttps://controlplane.tailscale.com. The third-party open-source control plane server implementation, Headscale, will serve our purposes nicely.
Unfortunately, the ControlURL change doesn't take effect until the Tailscale client is restarted. In practice, this will likely mean waiting for the whole machine to reboot – likely not an insurmountable restriction in the age of automatic Windows Update installation.
We can process Headscale's logs to automatically accept new machines that appear into our malicious tailnet: