Home / Articles / Web Security
Web Security

Login Successful, But Session Not Secure: How to Strengthen PHP Sessions

A correct password is not enough to keep an account secure. If PHP sessions are poorly managed, attackers can exploit session fixation, overly exposed cookies, or old sessions that are still valid.

Login Berhasil, tetapi Sesi Belum Aman: Cara Memperkuat Session PHP

After a user successfully logs in, many PHP applications immediately store user_id in $_SESSION. It seems simple, but this is where the user's identity starts to "stick" to the browser. If the session ID is not changed, cookies are not protected, or logging out only removes the display in the browser, the account can remain at risk even if the password is strong.

A session is a mechanism to remember that several HTTP requests come from the same user. The browser typically stores the session ID in a cookie, while the actual data is stored on the server. Since the session ID acts like an entry ticket, anyone who possesses it can potentially use that account as long as the ticket is still valid.

The Problem Is Not Just Stolen Session IDs

A frequently discussed attack is session hijacking, which occurs when an attacker obtains the victim's session ID. However, there is another equally important scenario: session fixation. In this attack, the attacker tries to make the victim use a session ID that the attacker already knows. If the application continues to use the same ID after login, the attacker can use that ID to log in as the victim.

OWASP recommends that the session ID be changed when the user's status changes, especially after successful authentication. PHP also documents that session_regenerate_id() should be called before authentication information is written to $_SESSION. A complete explanation is available in the OWASP Session Management Cheat Sheet and PHP session security documentation.

Four Layers of Protection to Check

1. Change Session ID After Login

Do not retain an anonymous session ID as the session ID of an authenticated account. After the username and password are verified, create a new ID first, then save the login status.

<?php
session_start();

if ($passwordValid) {
    session_regenerate_id(false);

    $_SESSION['user_id'] = $user['id'];
    $_SESSION['authenticated_at'] = time();
    $_SESSION['last_activity'] = time();
}
?>

This example intentionally uses false so that the old session is not aggressively deleted immediately. PHP documentation warns that directly deleting the old session can cause issues with unstable connections or concurrent requests. In production applications, mark the old session as expired and ensure that expired sessions are no longer accepted.

The session ID should also be changed when the user escalates privileges, for example, after re-entering the password before changing the email, enabling administrator features, or completing two-factor authentication.

2. Set Cookies with Security Attributes

Session cookies should not be left with default settings without checks. Three important attributes are:

  • Secure: cookies are only sent over HTTPS.
  • HttpOnly: JavaScript on the page cannot read cookies via document.cookie. This helps reduce the impact of cookie theft via XSS.
  • SameSite: limits the sending of cookies on cross-site requests and helps reduce CSRF risk.

A basic configuration in PHP might look like this:

<?php
session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax'
]);

session_start();
?>

Use SameSite=Strict if the application flow allows. However, Lax is often a more compatible choice for websites that still require certain cross-site navigation. This attribute is not a substitute for CSRF tokens, but rather an additional layer.

3. Do Not Include Sensitive Data in Session IDs

Session IDs should only be random identifiers, not containers for information. Do not create formats like user=andi|role=admin|expires=... and send them to the browser. Such information can be read, manipulated, or leaked through logs.

Store important data on the server, such as user ID, role, login time, and last activity time. Use PHP's built-in session generator or frameworks designed to produce random values. OWASP recommends session IDs that are unpredictable and do not contain personal information.

4. Implement Server-Side Timeout

Deleting cookies when the user presses the logout button does not necessarily end a session that has already been stolen. A copy of the cookie on another device can still be used if the server has not invalidated that session.

Therefore, keep track of the last activity time and check it on every request to pages that require login.

<?php
$timeout = 1800; // 30 minutes

if (!isset($_SESSION['user_id'])) {
    http_response_code(401);
    exit('Please log in.');
}

if (time() - ($_SESSION['last_activity'] ?? 0) > $timeout) {
    $_SESSION = [];
    session_destroy();
    http_response_code(401);
    exit('Session has expired.');
}

$_SESSION['last_activity'] = time();
?>

For high-risk activities, use a shorter timeout or ask the user to re-enter their password. Do not rely solely on session.gc_maxlifetime as the only expiration mechanism, as garbage collection settings are not a substitute for checking session status at the application level.

What About IP Address and User-Agent?

Binding sessions to IP addresses or User-Agents can help detect suspicious changes, but do not block users immediately whenever the values change. Mobile users may switch networks, pass through proxies, or use connections with changing IP addresses.

A safer approach is to treat such changes as risk signals. For example, request re-authentication if the session suddenly moves to another country, the User-Agent changes drastically, or there is administrator access from a new device. This is a design decision and risk analysis, not a one-size-fits-all rule for every website.

Checklist to Do Now

  • Ensure the website always uses HTTPS and session cookies have the Secure attribute.
  • Enable HttpOnly and choose SameSite=Lax or Strict according to the website flow needs.
  • Call session_regenerate_id() after login and before saving authentication status.
  • Limit idle session duration from the server side.
  • Invalidate sessions on the server when logging out or when passwords and access rights change.
  • Do not accept session IDs from URLs or GET parameters.
  • Log important events such as new logins, logouts, password changes, and expired sessions.

What Does This Mean for Us?

Session security is not an additional feature only needed by large websites. WordPress blogs with admin areas, small online stores, and internal PHP applications all use the same concept: the browser carries an identifier, and then the server decides whether that identifier is still valid.

The most important improvements are usually not writing an authentication system from scratch, but ensuring the flow is correct: use random session IDs, change IDs after login, protect cookies, limit session lifespan, and validate access rights on every action. A strong password helps open the door, but session management determines whether that door remains locked after the user logs in.

Sources & Further Reading

Explore Also

– Rio Yotto @rioyotto