Rotating a session you don't use
Reading a SessionsController#create I know doesn’t use the Rails session for
auth — auth lives in a separate signed cookie, pointing at a row in a
sessions table — I hit a line that stops me. reset_session. Right before
the real work. I sit with it for a second wondering if it’s dead code.
It isn’t. But the reason it isn’t is stranger than the standard explanation.
What the tutorial says it does
reset_session does two things. It empties the session hash — every key in
session[...] is wiped. And it rotates the session ID, so the cookie the
browser holds gets a new value. The canonical reason to call it at sign-in
is session fixation: an attacker pre-seeds a known session ID into a victim’s
browser — via a crafted link, an XSS payload, whatever — the victim
authenticates, and without rotation, the attacker’s copy of that ID is now
attached to an authenticated session.
The fix shows up in every tutorial: rotate the session ID at the moment of privilege escalation. Fine. Except this codebase doesn’t use the Rails session to carry privilege. A pre-seeded session ID grants the attacker nothing. The thing that actually proves the user is logged in is in a different cookie, pointing at a different row.
So why is reset_session there?
The attacker didn’t inject an ID. They injected state.
What session fixation is usually described as — attacker shares session ID, attacker gets in — is one specific case of a more general move. The attacker can pre-seed anything into the session hash before the victim shows up. Not just the ID. The whole blob. Keys I might check later. Flash values. CSRF tokens. Whatever fits.
Today, nothing in the controller stack reads those values for trust. Today, the rotated session ID is irrelevant because the auth boundary runs through a different cookie entirely. Today, the line reads like ceremony.
Today is a narrow window.
A defense for code I haven’t written yet
The session hash is mutable shared state every controller can poke. The
codebase will keep growing, and at some point someone will decide it’s fine
to stash a thing in session[...]. A locale, because it’s a convenient place
for per-user preferences. A “remember last viewed” value. A partially-filled
form the user started. A feature flag assignment. None of these feel
load-bearing in isolation. None of them, individually, would fail a security
review. They’re just conveniences.
And every one of them, at the moment it’s added, starts trusting that the
session state was set by the user’s browser, not by an attacker’s pre-seed.
Without reset_session at sign-in, that trust is unfounded. The attacker’s
pre-seeded values would still be sitting there after login, now attached to
an authenticated user, being read by code that has no idea where the data
actually came from.
reset_session isn’t defending today’s trust decisions. It’s defending
future trust decisions — the ones that start happening the day someone
types session[:something] = ... at the top of a controller and doesn’t
think twice about the provenance of that value.
Why the “we don’t use session auth” instinct gets this wrong
The mistake is easy to make because the reasoning sounds airtight. We don’t read the session for auth. An injected session doesn’t grant access. The attack doesn’t apply. Each sentence is true, individually. The failure is that they’re answering the wrong question. The question isn’t does this attack work against today’s code. The question is is any code — present or future — going to read this state.
The answer is almost always yes. The session hash is too convenient not to
be used. Someone will use it. They won’t check whether the contents could
have been attacker-controlled, because in their mental model, session[...]
is a thing the framework manages safely. reset_session is how you keep
that mental model honest.
There’s a generalization here I keep noticing. A lot of security hygiene isn’t about the current code; it’s about invariants that future code will rely on before any of that code has been written. Rotate on privilege change. Wipe stale buffers. Zero memory before freeing. None have a caller today. All have a caller eventually, and the defensive work has to have already happened by the time they show up.
A small thing I’d tell the next reader
If you see reset_session in a sign-in flow and your first thought is but
we don’t use the session for auth — good, you’re noticing the right thing.
The second thought is what matters. Somebody will. And by the time they do,
the rotation either already happened or it didn’t.
Leaving the line in is free. Removing it is a bet that nobody will ever
reach for session[...] with any expectation that its contents are theirs.
I wouldn’t take that bet.