Insufficient Session Expiration

Description

Insufficient Session Expiration occurs when a web application permits an attacker to reuse old session credentials or session IDs for authorization. This happens when sessions don't expire after a reasonable period of inactivity, don't expire upon logout, or when session tokens remain valid indefinitely. Long-lived sessions increase the window of opportunity for session hijacking attacks. Even after users believe they have logged out, their sessions may remain valid for attackers who have captured the session token.

Risk

Long-lived sessions significantly increase attack surface. Stolen session tokens remain usable for extended periods. Shared or public computers may retain valid sessions. Logout functionality that doesn't properly invalidate sessions gives users false security. Compliance frameworks require session timeout controls. Session tokens exposed in logs, browser history, or through XSS remain exploitable until they expire. Organizations have suffered breaches from old session tokens being exploited months after theft.

Solution

Implement absolute session timeouts (maximum session lifetime regardless of activity). Implement idle timeouts (expire after period of inactivity). Invalidate sessions server-side on logout. Regenerate session IDs on privilege changes (login, role change). Use secure session storage with proper expiration. Set appropriate cookie expiration. Consider implementing session binding to prevent token reuse from different contexts. For sensitive operations, require re-authentication.

Common Consequences

ImpactDetails
Access ControlScope: Session Hijacking

Long-lived sessions extend the window for captured tokens to be exploited.
ConfidentialityScope: Unauthorized Access

Attackers can access user accounts using old but still valid sessions.
Non-RepudiationScope: Accountability

Actions taken with hijacked sessions cannot be reliably attributed.

Example Code + Solution Code

Vulnerable Code

# VULNERABLE: No session timeout
from flask import Flask, session

app = Flask(__name__)
app.secret_key = 'secret'
# No session lifetime configured - sessions never expire!

@app.route('/login', methods=['POST'])
def login():
    # ... authenticate user ...
    session['user_id'] = user.id
    session['logged_in'] = True
    # Session lives forever!
    return redirect('/dashboard')

@app.route('/logout')
def logout():
    # Only clears client-side session data
    session.clear()
    # Server doesn't track invalidation
    # Old session cookie could still work!
    return redirect('/login')

# VULNERABLE: Permanent sessions
@app.route('/login', methods=['POST'])
def login_permanent():
    session.permanent = True  # Session lives for 31 days by default
    session['user_id'] = user.id
    # Way too long for sensitive applications
    return redirect('/dashboard')
// VULNERABLE: No session timeout in Spring
@Configuration
public class SessionConfig {
    // No session timeout configured
    // Sessions live until server restart
}

// VULNERABLE: Logout doesn't invalidate session
@Controller
public class AuthController {

    @PostMapping("/logout")
    public String logout(HttpSession session) {
        // Just removes attributes, doesn't invalidate
        session.removeAttribute("user");
        // Session ID still valid!
        return "redirect:/login";
    }
}

// VULNERABLE: Long session timeout
@Configuration
public class InsecureSessionConfig implements WebMvcConfigurer {

    @Bean
    public ServletContextInitializer servletContextInitializer() {
        return servletContext -> {
            // 7 days session timeout - too long!
            servletContext.setSessionTimeout(10080);
        };
    }
}
// VULNERABLE: JWT without expiration
const jwt = require('jsonwebtoken');

function generateToken(user) {
    // No expiration set!
    return jwt.sign({ userId: user.id }, secret);
}

// VULNERABLE: Long-lived tokens
function generateTokenWithExpiry(user) {
    return jwt.sign(
        { userId: user.id },
        secret,
        { expiresIn: '365d' }  // Valid for a year!
    );
}

// VULNERABLE: Express session without timeout
app.use(session({
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: true,
    cookie: {
        secure: true
        // No maxAge - session cookie but no server-side expiry!
    }
}));

// VULNERABLE: Logout doesn't destroy session
app.post('/logout', (req, res) => {
    req.session.user = null;  // Just nulls the user
    // Session still exists and could be reused
    res.redirect('/login');
});

Fixed Code

# SAFE: Proper session timeouts
from flask import Flask, session
from datetime import timedelta

app = Flask(__name__)
app.secret_key = os.urandom(32)

# Configure session lifetime
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=30)  # Absolute timeout
app.config['SESSION_REFRESH_EACH_REQUEST'] = True  # Reset on activity

# Track session validity server-side
active_sessions = {}  # Use Redis in production

@app.route('/login', methods=['POST'])
def login():
    # ... authenticate user ...

    # Regenerate session ID on login
    session.clear()
    session.permanent = True
    session['user_id'] = user.id
    session['created_at'] = datetime.utcnow().isoformat()
    session['session_id'] = str(uuid.uuid4())

    # Track on server
    active_sessions[session['session_id']] = {
        'user_id': user.id,
        'created_at': datetime.utcnow(),
        'last_activity': datetime.utcnow()
    }

    return redirect('/dashboard')

@app.before_request
def check_session_validity():
    if 'session_id' in session:
        server_session = active_sessions.get(session['session_id'])

        if not server_session:
            # Session invalidated server-side
            session.clear()
            return redirect('/login')

        # Check idle timeout (15 minutes)
        idle_time = datetime.utcnow() - server_session['last_activity']
        if idle_time > timedelta(minutes=15):
            invalidate_session(session['session_id'])
            session.clear()
            return redirect('/login?reason=idle_timeout')

        # Check absolute timeout (8 hours)
        session_age = datetime.utcnow() - server_session['created_at']
        if session_age > timedelta(hours=8):
            invalidate_session(session['session_id'])
            session.clear()
            return redirect('/login?reason=session_expired')

        # Update last activity
        server_session['last_activity'] = datetime.utcnow()

@app.route('/logout')
def logout():
    session_id = session.get('session_id')
    if session_id:
        invalidate_session(session_id)
    session.clear()

    response = redirect('/login')
    # Clear session cookie
    response.delete_cookie('session')
    return response

def invalidate_session(session_id):
    active_sessions.pop(session_id, None)
// SAFE: Proper session configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .invalidSessionUrl("/login?expired")
                .maximumSessions(1)  // Prevent concurrent sessions
                .maxSessionsPreventsLogin(false)
                .expiredUrl("/login?expired")
            .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .invalidateHttpSession(true)  // Properly invalidate
                .deleteCookies("JSESSIONID")
                .clearAuthentication(true);
    }
}

// SAFE: Session timeout configuration
@Configuration
public class SessionConfig {

    @Bean
    public ServletContextInitializer servletContextInitializer() {
        return servletContext -> {
            // 30 minute timeout
            servletContext.setSessionTimeout(30);
        };
    }
}

// SAFE: Proper logout handler
@Controller
public class SecureAuthController {

    @PostMapping("/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) {
        HttpSession session = request.getSession(false);

        if (session != null) {
            // Log logout event
            auditService.logLogout(session.getAttribute("userId"));

            // Invalidate session on server
            session.invalidate();
        }

        // Clear cookies
        Cookie sessionCookie = new Cookie("JSESSIONID", null);
        sessionCookie.setMaxAge(0);
        sessionCookie.setPath("/");
        response.addCookie(sessionCookie);

        return "redirect:/login?logout";
    }

    @PostMapping("/login")
    public String login(HttpServletRequest request, @RequestBody LoginRequest loginRequest) {
        // Invalidate existing session
        HttpSession oldSession = request.getSession(false);
        if (oldSession != null) {
            oldSession.invalidate();
        }

        // Authenticate
        User user = authService.authenticate(loginRequest);

        // Create new session with new ID
        HttpSession newSession = request.getSession(true);
        newSession.setAttribute("userId", user.getId());
        newSession.setAttribute("createdAt", System.currentTimeMillis());
        newSession.setMaxInactiveInterval(1800);  // 30 minutes

        return "redirect:/dashboard";
    }
}
// SAFE: JWT with proper expiration and refresh
const jwt = require('jsonwebtoken');

// Short-lived access token
function generateAccessToken(user) {
    return jwt.sign(
        { userId: user.id, type: 'access' },
        accessSecret,
        { expiresIn: '15m' }  // 15 minutes
    );
}

// Longer-lived refresh token (stored securely)
function generateRefreshToken(user) {
    const token = jwt.sign(
        { userId: user.id, type: 'refresh', tokenId: uuid.v4() },
        refreshSecret,
        { expiresIn: '7d' }
    );

    // Store in database for revocation capability
    saveRefreshToken(user.id, token);

    return token;
}

// Token refresh endpoint
app.post('/token/refresh', async (req, res) => {
    const { refreshToken } = req.body;

    try {
        const payload = jwt.verify(refreshToken, refreshSecret);

        // Check if token is revoked
        if (await isTokenRevoked(payload.tokenId)) {
            return res.status(401).json({ error: 'Token revoked' });
        }

        // Generate new access token
        const accessToken = generateAccessToken({ id: payload.userId });
        res.json({ accessToken });

    } catch (err) {
        res.status(401).json({ error: 'Invalid refresh token' });
    }
});

// SAFE: Express session with proper timeouts
const session = require('express-session');
const RedisStore = require('connect-redis')(session);

app.use(session({
    store: new RedisStore({ client: redisClient }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    rolling: true,  // Reset expiry on activity
    cookie: {
        secure: true,
        httpOnly: true,
        sameSite: 'strict',
        maxAge: 30 * 60 * 1000  // 30 minutes
    }
}));

// SAFE: Proper logout
app.post('/logout', (req, res) => {
    const sessionId = req.sessionID;

    req.session.destroy(err => {
        if (err) {
            console.error('Session destruction error:', err);
        }

        // Revoke any refresh tokens
        revokeUserRefreshTokens(req.user?.id);

        // Clear cookie
        res.clearCookie('connect.sid', {
            path: '/',
            httpOnly: true,
            secure: true,
            sameSite: 'strict'
        });

        res.json({ success: true });
    });
});

// Middleware to check session validity
app.use((req, res, next) => {
    if (req.session?.userId) {
        const sessionAge = Date.now() - (req.session.createdAt || 0);
        const maxAge = 8 * 60 * 60 * 1000;  // 8 hours absolute

        if (sessionAge > maxAge) {
            return req.session.destroy(() => {
                res.status(401).json({ error: 'Session expired' });
            });
        }
    }
    next();
});

Exploited in the Wild

GitHub Session Token Theft (Multiple)

Long-lived GitHub personal access tokens have been stolen from developer machines and used months later to access private repositories.

JWT Token Reuse Attacks

Multiple applications using JWTs without expiration or with very long expiration have suffered from token reuse attacks where stolen tokens remained valid indefinitely.

Corporate VPN Session Persistence

VPN solutions with insufficient session expiration have allowed attackers with stolen session cookies to maintain persistent access to corporate networks.


Tools to test/exploit


CVE Examples


References

  1. MITRE. "CWE-613: Insufficient Session Expiration." https://cwe.mitre.org/data/definitions/613.html

  2. OWASP. "Session Management Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html