The alert I was chasing had nothing to do with what I found. I was in the codebase for an unrelated ticket, tracing how staff sessions get verified on an internal admin tool, when I hit a line that made the whole page redirect logic stop mattering: if("login" == p) return;. That’s the login exemption, checked before the session check, and 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.

I didn’t take that on faith. I 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. Same bypass worked against a second, unrelated tool. Then I checked how many pages shared this gate: 103 files under tools/, another 122 staff pages outside it, 46 more under ajax/, all routed through the same SessionStaffVerifyRedirect() function. One missing check, one shared include, 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 itself 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, not on anything the server actually controls. 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.

I patched a clone, then differential-tested it against the unpatched one side by side: patched tool with ?p=login now 302s to login; unpatched still leaks 200 and data. Login page itself still rendered fine. Shipped it as a hotfix, verified the bypass was closed on production the same hour, confirmed staff who were already logged in saw no change. Ticket closed, felt clean.

The guard didn’t know it was also blocking the door it guarded

Thirteen hours later, staff couldn’t log in. Not “session expired,” not a typo, actual login attempts, correct credentials, bouncing straight back to the login page with error=SessionExpired in the URL. I went to reproduce it and found I couldn’t log into the staff portal on any clone either, which is not something I’d normally suspect from a same-day hotfix, so I burned a few minutes wondering if it was a credential or cache issue before I looked at the actual response.

It wasn’t the credentials. 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, meaning the password was never even checked. My 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. I’d fixed the render and broken the submit, because I’d reasoned about “the login page” as one thing when it was two files sharing one gate, and I only checked one of them.

Nobody had been locked out yet when I found it because anyone with an existing session was unaffected, staff who’d logged in before the hotfix just kept working. It was quiet by luck, not because it was working.

The fix was the same shape as the first one, just wider: exempt both default.asp and ajax/login.asp by path, still keyed off session.pathInfo, still not spoofable by a query param. I verified it the same way as before: a bad-credentials POST now correctly reached AuthenticateStaff and came back error=invalidLogin instead of SessionExpired, a real staff login succeeded end to end, and the original ?p=login bypass on an unrelated tool still redirected, no regression. Verified live on prod before calling it closed, the same way I’d verified the hole in the first place, because “I’m pretty sure” was exactly the assumption that put me here.

The mistake wasn’t skipping a test. It was treating “authenticate before serving” as one rule when the real rule is per-entry-point: every path that reaches the auth gate needs its own accounting of what it’s supposed to do when there’s no session yet, including the one thing whose whole job is to create one.