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:
- The debug panel listed every recent request as a tag, and each tag exposed that user's session data.
- Session cookies were not marked HttpOnly, so the values were readable rather than hidden from scripts and panels.
- Cookies were not marked Secure, so they traveled over plain HTTP and were trivial to capture.
- Strict session mode was off, which means the server accepted any session identifier a client presented rather than only ones it had issued.
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
- 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.
- 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.
- 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.
- 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
- For every app, request the debug or profiler endpoint your framework ships. Then request it again with a localhost forwarded header. Any response that isn't a flat refusal is a finding.
- Inspect your session cookies in the browser. Confirm HttpOnly, Secure, and SameSite are set, and confirm with your framework that strict session handling is enabled.
- Grep your production configuration for the environment value that controls debug mode. If it's anything other than the production setting, treat it as an incident, not a cleanup task.
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.