In Part 1, we looked at the basics:
How sessions work, why cookies matter, and why attributes like HttpOnly, Secure, and SameSite are important.
But secure cookie settings are only one part of session security.
A session has a lifecycle.
It is created, authenticated, used, refreshed, expired, and eventually destroyed.
Problems can happen at any point in that lifecycle.
In this post, we'll look at some of the less obvious session security issues developers should understand.
CSRF: When the Browser Helps the Attacker
One of the strange things about cookies is that browsers send them automatically.
That is convenient for users.
It is also exactly why CSRF exists.
Imagine a user is logged into:
bank.example
Their browser has:
Cookie: session_id=a8f19d27c4e91b72
Now the user visits:
evil.example
That page contains something like:
<form action="https://bank.example/transfer" method="POST">
<input type="hidden" name="amount" value="500">
<input type="hidden" name="to" value="attacker">
</form>
<script>
document.forms[0].submit();
</script>
The attacker does not need to know the victim's session cookie.
The browser may attach it automatically when sending the request to bank.example.
From the server's perspective, the request could arrive with a valid authenticated session.
That is the core idea behind Cross-Site Request Forgery.
The attacker is not stealing the session.
They are abusing the browser's ability to use it.
CSRF Tokens
One common defense is a CSRF token.
The application generates a random value associated with the user's session.
For example:
<form method="POST" action="/settings/email">
<input
type="hidden"
name="csrf_token"
value="9bf231ce89f5d10a"
>
<input
type="email"
name="email"
>
<button type="submit">
Change email
</button>
</form>
The server expects both:
A valid authenticated session
and
A valid CSRF token
If the attacker can cause the browser to submit a request but cannot obtain the correct CSRF token, the server can reject it.
Conceptually:
Session cookie
+
CSRF token
↓
Request accepted
A missing or incorrect token should cause the request to fail.
SameSite cookies can also reduce CSRF risk, but it is useful to understand CSRF as its own problem rather than thinking one cookie flag solves everything.
Session Fixation
Another session attack is less intuitive.
It is called session fixation.
Normally, we think of an attacker trying to steal a session after login.
Session fixation reverses that idea.
The attacker tries to make the victim use a session ID the attacker already knows.
Imagine the attacker knows:
session_id=XYZ123
The victim somehow begins using that session.
Then the victim logs in.
If the application keeps the same session identifier after authentication:
Before login:
session_id=XYZ123
After login:
session_id=XYZ123
the attacker already knows the identifier of the authenticated session.
The attack flow looks like this:
Attacker knows session XYZ123
↓
Victim begins using XYZ123
↓
Victim logs in
↓
Server authenticates XYZ123
↓
Attacker still knows XYZ123
The password was never stolen.
The session was never guessed.
The problem was that the session identifier was not changed when authentication happened.
Regenerate the Session ID After Login
The defense is straightforward:
Generate a new session identifier when authentication state changes.
For example:
app.post("/login", async (req, res) => {
const user = await authenticate(
req.body.username,
req.body.password
);
if (!user) {
return res.status(401).send("Invalid credentials");
}
req.session.regenerate((error) => {
if (error) {
return res.sendStatus(500);
}
req.session.userId = user.id;
req.session.username = "khg5293";
res.redirect("/dashboard");
});
});
The important transition is:
Unauthenticated session ID
↓
User logs in
↓
New session ID
↓
Authenticated session
The old identifier should not simply become the authenticated session.
Session rotation can also make sense after important privilege changes.
For example:
User
↓
Admin role granted
↓
Rotate session ID
A session should not quietly carry sensitive state transitions forever.
Session Expiration
Sessions should not remain valid indefinitely.
If a session ID is stolen today and still works six months later, the attacker gets a very large window of opportunity.
Two common expiration strategies are:
Idle Timeout
The session expires after a period of inactivity.
For example:
Last activity: 10:00 AM
No activity for 30 minutes
Session expires at 10:30 AM
If the user makes another legitimate request before the timeout:
10:00 AM request
10:20 AM request
10:40 AM request
the idle timer may be refreshed.
This protects against sessions that are abandoned but never explicitly logged out.
Absolute Timeout
An absolute timeout limits the total lifetime of the session.
For example:
Login: 9:00 AM
Absolute lifetime: 8 hours
Session expires: 5:00 PM
Even if the user remains active, the session eventually ends.
This creates an upper bound on how long the same authenticated session can exist.
A system may use both:
Idle timeout:
30 minutes
Absolute timeout:
8 hours
The correct values depend on the application.
A low-risk community forum and an online banking system should not necessarily use the same session lifetime.
Remember Me Changes the Risk
Many applications offer a feature like:
Remember me
That can be useful, but developers should understand what it means from a security perspective.
A normal session might last:
30 minutes
while a persistent login might last:
30 days
That dramatically increases the usefulness of a stolen token.
If long-lived authentication is required, it is often better to treat it as a separate mechanism rather than simply making the normal session live forever.
For example:
Short-lived session
+
Long-lived reauthentication token
The persistent token can be separately rotated, revoked, and monitored.
Longer lifetime should always be treated as increased exposure.
Logout Should Destroy the Session
A common logout implementation is:
res.clearCookie("session_id");
res.redirect("/");
That removes the cookie from the user's browser.
But ask an important question:
What happened to the session on the server?
Possibly nothing.
Imagine the server still contains:
sessions["a8f19d27c4e91b72"] = {
userId: "khg5293",
authenticated: true
};
The original browser may no longer have the cookie.
But if an attacker already copied the session identifier, that copy might still work.
Logout should normally invalidate the server-side session too.
For example:
app.post("/logout", (req, res) => {
req.session.destroy((error) => {
if (error) {
return res.sendStatus(500);
}
res.clearCookie("session_id");
res.redirect("/");
});
});
The goal is:
Delete browser cookie
+
Destroy server session
Both matter.
Password Changes Should Affect Existing Sessions
Another important lifecycle question is:
What happens to existing sessions when the password changes?
Imagine this situation:
Laptop session
Phone session
Tablet session
Unknown attacker session
The user changes their password.
If all existing sessions remain valid, the attacker's session may continue working.
Depending on the application, a password reset or other major account security event may justify invalidating other sessions.
For example:
Password reset
↓
Invalidate old sessions
↓
Require new authentication
This is especially important after suspected account compromise.
Session Revocation Matters
Expiration answers:
When should this session naturally stop working?
Revocation answers:
How can we make this session stop working right now?
Useful scenarios include:
User clicks "log out everywhere"
Password reset
Account locked
Admin disables account
Suspicious login detected
User removes a trusted device
A server-side session store makes this relatively straightforward.
If the session no longer exists:
session_id=a8f19d27c4e91b72
the server should reject it.
For example:
HTTP/1.1 401 Unauthorized
A session is only useful while the server still considers it valid.
Don't Put Session IDs in URLs
This is worth stating directly.
Do not design authenticated URLs like:
https://example.com/account?session_id=a8f19d27c4e91b72
URLs leak into many places.
For example:
Browser history
Server logs
Proxy logs
Analytics systems
Screenshots
Copied links
Bookmarks
Referer headers
A URL is a terrible place for a bearer credential.
Authentication tokens belong in mechanisms designed to transport them safely.
For traditional browser sessions, that generally means cookies.
Avoid Logging Session Tokens
The same idea applies to application logging.
This is useful:
2026-09-15T21:42:11Z
event=login_success
user=khg5293
source_ip=192.0.2.18
This is dangerous:
2026-09-15T21:42:11Z
event=login_success
user=khg5293
session_id=a8f19d27c4e91b72
Logs are often copied, indexed, retained, exported, and accessed by many systems.
A live session identifier appearing in logs can become a credential leak.
Application logs should avoid exposing secrets unnecessarily.
Keep Cookie Scope Narrow
Cookie attributes can control where a cookie is sent.
For example:
Set-Cookie: session_id=abc123; Path=/
sends the cookie more broadly than:
Set-Cookie: session_id=abc123; Path=/account
The correct scope depends on the application.
The same idea applies to domains.
An overly broad cookie configuration can cause a credential to be sent to places that do not actually need it.
A useful security principle is:
Give credentials the smallest scope they actually require.
That applies to cookies too.
Avoid Unnecessary Session Data in the Browser
In a traditional server-side session architecture, the browser does not need to know everything about the session.
This is usually enough:
Cookie: session_id=a8f19d27c4e91b72
The server can associate that identifier with:
{
userId: "khg5293",
role: "user",
permissions: ["profile:read"],
authenticated: true
}
There is usually no benefit in exposing sensitive server-side session state to the browser unnecessarily.
Keep the client-side value minimal.
Treat Session Security as a Lifecycle
It is easy to focus entirely on login.
But a secure session has many stages.
Session created
↓
User authenticates
↓
Session ID rotated
↓
Session used
↓
Session monitored
↓
Session expires or is revoked
↓
Session destroyed
Security decisions exist at every step.
If any one step is poorly designed, the session can become easier to abuse.
A More Complete Session Security Checklist
For a traditional web application:
[ ] Generate unpredictable session IDs
[ ] Rotate session IDs after authentication
[ ] Rotate again after major privilege changes where appropriate
[ ] Use HttpOnly
[ ] Use Secure
[ ] Choose an appropriate SameSite policy
[ ] Protect state-changing requests against CSRF
[ ] Implement idle expiration
[ ] Consider an absolute session lifetime
[ ] Invalidate sessions during logout
[ ] Revoke sessions after important account security events
[ ] Support session revocation where appropriate
[ ] Never place session IDs in URLs
[ ] Avoid logging authentication tokens
[ ] Keep cookie scope as narrow as practical
[ ] Avoid exposing unnecessary session state to the browser
Final Thought
Session security is not just about protecting one cookie.
It is about controlling the entire lifecycle of an authenticated identity.
The session has to be created securely.
It has to change when authentication changes.
It has to expire.
It has to be revocable.
And when the user logs out, it should actually stop working.
The safest way to think about a session ID is still the simplest one:
If someone possesses this value,
what can they do?
If the answer is:
Act as the authenticated user
then that session deserves the same care as any other credential.
Because once the password has already done its job, the session becomes the thing standing between the user and everyone else.
Top comments (0)