The debug toolbar that handed out other people's sessions

Every web framework ships a developer mode. It's wonderful while you're building: it shows you the exact SQL that ran, the request and response in full, the session contents, the timing of every call. It's meant to live on your laptop and die before production. The trouble is that the switch that turns it on is one environment variable, and environment variables get copied.

On a recent test of a crypto exchange's API, that switch was still on in production. The framework's debug toolbar was live, indexing real traffic, and it took about two minutes and zero credentials to ride it into other people's accounts.

Severity: CVSS 8.8 (High) · CWE-489, CWE-348
Exposed: a rolling feed of live customer sessions (a continuous stream, not a single victim), enabling password-less account takeover.

How a debug panel becomes an account takeover

A debug toolbar keeps a record of recent requests so you can inspect them. In production, "recent requests" means real customers' requests. Each entry holds everything the developer wanted to see while debugging, including the session of the user who made the call.

The panel was supposed to be protected. It had an allowlist that only let localhost view it. But the allowlist trusted a header. Adding X-Forwarded-For: 127.0.0.1 to the request was enough to convince the server that the visitor was local, and that header is attacker-controlled. The allowlist was real; the client got to fill in the one value it checked.

Once inside, the chain wrote itself, because a stack of session settings were all turned the wrong way at the same time:

Put those together. Read a real user's session identifier out of the debug panel, set it as your own cookie, and the server treats you as that user. No password, no token, no second factor. Just a value copied from a page that should never have been reachable.

Why each setting matters

It's tempting to call any one of these a minor hardening item, and individually a triager might shrug. The point is that security here was supposed to be layered, and every layer was disabled at once.

HttpOnly exists so that even if something can run in the page, it cannot read the cookie. Secure exists so the cookie never crosses an unencrypted link. Strict session mode exists so that an attacker cannot simply invent or replay a session identifier. Each of these would have broken the chain by itself. With all of them off, the debug panel was not just an information leak. It was a working login as anyone who had used the site recently.

Finding it

There's not much to it. Look for the framework's debug or profiler path, the well-known one that ships with whatever stack the app is built on. Request it. If you get a permission error, try the localhost header bypass, because IP allowlists that read a forwarded header are extremely common. If the panel renders, you're looking at production traffic. From there, pull a session identifier from any recent request, set it as your cookie, and reload an authenticated page as that user.

# debug/profiler live in prod; the IP allowlist trusts a forwarded header:
$ curl -s https://<target>/_profiler/latest -H 'X-Forwarded-For: 127.0.0.1' \
    | grep -oE 'session_id=[a-z0-9]+'
session_id=9f3c1a...        # the last real visitor's session

# cookies weren't HttpOnly/Secure and strict mode was off, so replay just works:
$ curl -s https://<target>/account -H 'Cookie: session_id=9f3c1a...'
HTTP/2 200                  # logged in as that user, no password

The whole thing uses intended features of the framework. Nothing is exploited in the memory-corruption sense. It's a door left open, behind a lock that trusts the attacker to be honest about where they're coming from.

The blast radius

Session level account takeover on a financial platform is close to the worst non fund draining outcome there is. With a victim's session an attacker can read balances and transaction history, see and change account settings, initiate whatever actions the app allows a logged in user to take, and harvest personal data wholesale by cycling through the recent requests the panel keeps listing. Because the panel shows a rolling window of live traffic, the attacker does not target one victim. They get a feed of fresh sessions for as long as the panel stays up.

The fix

  1. Turn debug and profiler modes off in production, and make it impossible to turn them on by accident. The environment flag that enables them should be asserted off at boot, and the build for production should not include the toolbar at all where the framework allows that.
  2. Stop trusting forwarded headers for access decisions. If a panel must be restricted by IP, enforce it at the network or load balancer layer using the real connection address, not an application header a client can set.
  3. Set the session cookie flags correctly and keep them that way: HttpOnly on, Secure on, and strict session mode on so the server only honors identifiers it issued.
  4. Add a production smoke test that requests the debug path and fails the deploy if it returns anything but a hard 404 or 403.

Prevent it in CI/CD: assert debug-off at boot and drop the toolbar from the production build entirely; enforce IP allowlists at the load balancer on the real connection address, never an application header; a post-deploy smoke test that fails the release if the debug path returns anything but 404/403; and cookie flags (HttpOnly, Secure, strict) set as policy.

Three things to check on your own apps

A developer tool shows you everything by design. Leave one running in production and it shows everything to everyone, including the sessions of the customers who trusted you with their money.