Report · 1 August 2026

Four bugs we found in ourselves

An internal review of the surfaces we had added over the previous month found four issues: a hosted headless browser that would follow a user-supplied URL to cloud metadata, an unauthenticated registration endpoint with no ceiling, a re-registration path that did not require the original secret, and a response reader with no size limit. All four are fixed. Published because a security vendor that only reports other people's bugs is telling you half of something.


Contents

We build a tool that finds vulnerabilities in other people's software. That is a bad reason to assume there are none in ours, and a good reason to expect the opposite, because everything we ship is exactly the kind of thing we spend all day breaking: a control plane that accepts user-supplied URLs, a public listener, a raw HTTP client.

On 1 August 2026 we reviewed everything added in the preceding month. Four issues, all fixed. Here they are with enough detail to be useful, including the one we consider genuinely serious.

High: our login helper was a request-forgery proxy

Testing an authenticated application needs a session, and for logins that require a real browser we ran a headless one on our own infrastructure. You give it the addresses involved in your login flow, it drives the browser through them, and it hands back a token.

It took four URLs from the user and validated none of them. It navigated to whatever it was given, and posted to whatever it was given.

So an authenticated customer could save a credential whose login address pointed not at their own application but at something inside our network, or at the cloud metadata address that most infrastructure providers expose:

authorize_url: http://169.254.169.254/latest/meta-data/iam/...

That address answers only to requests originating inside the instance, which is precisely what our browser was. On a misconfigured instance it returns credentials. The feature was, in effect, a request-forgery proxy that we hosted, authenticated, and pointed wherever a customer asked.

Warning

We have no evidence this was used. That is not the reassurance it sounds like: at the time, nothing in the system would have recorded it if it had been. The absence of evidence was itself part of the finding.

The fix is a check on all four fields before the browser is started, and it fails closed: an address that cannot be resolved and classified is rejected rather than allowed through. It refuses loopback, link-local, the metadata address, private ranges and anything that is not ordinary HTTP.

Our own local testing broke immediately, because it had been pointing the broker at a private address on the machine's container network. That was the correct outcome and we want to record it, because the temptation at that moment is to add an exception for development and that exception is how this class comes back. Local testing now runs against a real external provider instead.

Three things we recommended that are not code, and are worth doing regardless of whether your application has this bug: restrict the egress of anything that fetches user-supplied URLs so it cannot reach private ranges at all, require the newer metadata service protocol with a hop limit that prevents proxied access, and do not run a browser with its sandbox disabled unless you have exhausted the alternatives.

The same component was breaking a separate architectural rule of ours by reaching customer targets from our addresses. That, and what we did about it, is in whose IP address is your scanner using?

Medium: an unauthenticated endpoint with no ceiling

Out-of-band confirmation needs a listener that anybody's target can reach, so its registration endpoint is deliberately public and unauthenticated. It accepted registrations without limit and held them in memory.

Registering in a loop therefore consumed memory until the process died. No authentication to obtain, no rate to exceed, nothing clever required.

A ceiling now applies. The wider lesson is the one we keep relearning: an endpoint that is public by design tends to skip the review that public-by-accident endpoints get, because its openness reads as intentional and the reviewer moves on. Deliberately public is the case that needs the most care, not the least.

Low: re-registration did not require the original secret

Each registration issues a secret. Re-registering the same identifier did not require presenting it, so anyone who learned an identifier could re-key it.

Rated low because the identifiers are random and short-lived, and the practical impact is disruption rather than disclosure. Fixed in the same change as the ceiling. We are including it because a report that lists only the interesting findings is a report that has been curated, and once you know it has been curated you cannot tell what else was left out.

Medium: a response reader with no size limit

Parts of the scanner speak HTTP directly rather than through a library, because testing malformed requests means being able to send things a well-behaved client will not. That code read responses to completion with no cap.

A target that streams indefinitely, whether hostile or simply broken, would grow the scanner's memory until it was killed. The scanner is the component pointed at things you do not trust, which makes this the wrong place to assume good behaviour.

Reads are now capped at 8 MiB. A truncated response is a bad result, and a dead scanner mid-scan is a worse one.

The pattern across all four

Three of the four are the same omission: a boundary where input arrives from somewhere untrusted, and no limit on what that input is allowed to do. A URL with no restriction on where it points. A registration with no ceiling. A read with no size. None of them is a subtle bug and none required insight to find. They required someone to go and look.

They also cluster in one place, which is the part we would encourage you to steal. Every one of these was in code written in the previous four weeks. New surface is where the bugs are, not because recent code is worse, but because it has been read fewer times. Reviewing on a schedule finds old bugs. Reviewing whatever you added last month finds the ones that are actually there.

Writing tests afterwards turned up a close relative of the first finding, and the reason our authorization suite had failed to catch any of this. Both are in five of our authorization tests were passing for the wrong reason.

← All publications