One line was sitting in the staff session check: if("login" == p) return;. We were in the codebase for an unrelated ticket, tracing how staff sessions get verified on an internal admin tool, and it made the page redirect logic we’d come to read stop mattering. That’s the login exemption, and it runs before the session check. p is a raw query-string parameter. Append ?p=login to any tool URL and the gate returns without ever looking at whether you’re staff.
We didn’t take that on faith. We hit it live. An anonymous GET to a diagnostic tool with no cookies came back 302 to the login page, as expected. The same request with ?p=login appended came back 200, with 6,375 bytes of live database rows in the body, cart items with real IDs and statuses. The same bypass worked against a second, unrelated tool. Then we counted how many pages shared this gate: 103 files under tools/, another 122 staff pages outside it, and 46 more under ajax/, all routed through the same SessionStaffVerifyRedirect() function. One missing check in one shared include left the entire internal admin surface open to anyone who knew the parameter name.
The exemption existed for a real reason. Without it, the login page would redirect-loop, since rendering it also runs the session check that says you’re not logged in. But it was keyed on a value the client sends, and the server has no say in that value. The fix was to stop trusting p and check the resolved path instead, session.pathInfo, which IIS sets from the actual file being served and which a query string can’t touch. Only default.asp, the login page itself, would get the exemption.
We patched a clone, then differential-tested it against the unpatched one side by side. The patched tool with ?p=login now 302s to login, and the unpatched one still leaks 200 and data. The login page itself still rendered fine. We shipped it as a hotfix, verified the bypass was closed on production the same hour, and confirmed that staff who were already logged in saw no change. We closed the ticket and considered it clean.
Thirteen hours later, correct passwords bounced
Staff couldn’t log in. These were real login attempts with correct credentials, bouncing straight back to the login page with error=SessionExpired in the URL. We went to reproduce it and found we couldn’t log into the staff portal on any clone either. A credential problem or a stale cache would be the usual suspect, and we burned a few minutes on exactly that before we looked at the actual response.
The credentials were fine. A POST to the login handler with deliberately wrong credentials still came back 302 to error=SessionExpired instead of reaching the actual authentication function. The guard was firing before AuthenticateStaff ever ran, so the password was never even checked. Our path-based fix from earlier that day only exempted default.asp, the page that renders the login form. But the form doesn’t POST to default.asp. It posts to /ajax/login.asp, a different file, which runs through the exact same SessionStaffVerifyRedirect() on the way in. We’d fixed the render and broken the submit. We had reasoned about “the login page” as one thing when it was two files sharing one gate, and we only checked one of them.
Nobody had been locked out yet when we found it, because anyone with an existing session was unaffected, and staff who’d logged in before the hotfix just kept working. It stayed quiet by luck, not because it was working.
The fix had the same shape as the first one, just wider. We exempted both default.asp and ajax/login.asp by path, still keyed off session.pathInfo, still not spoofable by a query param. We verified it the same way as before. A bad-credentials POST now reached AuthenticateStaff and came back error=invalidLogin instead of SessionExpired. A real staff login succeeded end to end. The original ?p=login bypass on an unrelated tool still redirected, so no regression. We verified live on prod before calling it closed, the same way we’d verified the hole in the first place, because “we’re pretty sure” was exactly the assumption that put us here.
How the review question was framed
The review that found this started from a question nobody could answer cleanly: where, exactly, does the login check happen on this surface? An answer like “it’s probably fine because everything routes through the same shared include” didn’t count. It was filed as a read-only investigation with no code change expected. It found that the one exemption in the check, there so the login page could render, was conditioned on a query-string value, which whoever sends the request controls. The fix checks the file the server resolved and is about to serve.
Every entry point gets its own accounting
Every path that reaches the auth gate needs its own accounting of what it must do when there is no session yet, including the login handler whose whole job is to create one. When a shared gate changes, test each entry point live, with both a failed and a successful login, before calling the fix done.