A customer’s session-timeout message named the wrong cause. It said their session had been set to expire in a year. The session that actually died belonged to a different account, one on a much shorter default. The dialog was reporting one account’s setting as the reason for another account’s logout.
That reads like a copy bug: swap the message, point it at the right value, close the ticket. We traced the whole path before touching anything, because a dialog confusing state between two accounts meant something upstream was doing the same thing.
What the trace found
The redirect that sends a person to the session-timeout page carries the real reason as a URL parameter. Nothing downstream ever reads it. The dialog shows whatever the current session’s own setting is, and after a fresh login that is the new session, not the one that actually timed out. That explained the wrong message. The same trace also turned up something the ticket never mentioned.
The login page builds its form action from the entire incoming query string, unexamined, and hands the whole thing to the authentication endpoint on submit. One of the values riding along in that query string is a redirect target. After credentials are accepted, the endpoint sends the browser there with no check on host, scheme, or leading slash. The only step between that value and the browser makes sure it is a string, which sanitizes nothing.
That is an open redirect. A link to the real login page, on the real domain, with the real certificate, can carry a hidden destination that fires the instant a real login succeeds. Every signal a person is taught to check, the domain, the padlock, the login form itself, is genuine right up to the moment they get sent somewhere else entirely.
Two smaller problems came out of the same trace:
- Sessions that timed out were never marked expired in storage. If a timeout setting was raised later, an already-dead session could come back to life.
- A settings page offering a fixed list of session lengths had no fallback for accounts holding a value outside that list. Opening the page and saving without changing anything silently dropped those accounts to the shortest setting on the list. 172 accounts held values outside the offered list.
The first fix covered one login flow
We shipped a fix the same night. It required a real path on the redirect target, wrote a true expiry flag instead of leaving the row valid, and put an allow-list on the session-length setting in both directions. We scoped the redirect fix to the login flow the original report pointed at, because that was where the bug lived from the customer’s side.
That scoping was wrong. The identical redirect logic existed on a second login flow, one for accounts with meaningfully more access than a customer account, and it was completely unguarded. The code that routes an expired session to the right page sends that account type through exactly that flow.
We found it by going back over the fix in the same sitting, minutes after shipping it. A second, narrower release closed the gap before the night was over.
Search for the shape of the code
A fix for a pattern has to cover every place the pattern appears. That means searching the whole codebase for that shape of code. The one flow a bug report happened to name only tells you where to start. That search would have taken a few minutes done first. Running it after shipping is the only reason this closed in one night instead of staying open.
A message that looks like a wording error can point to a deeper fault, so trace the full path before closing the ticket. Then review the fix again right after it goes out.