Unzureichende Session-Ablaufzeit

Beschreibung

Unzureichende Session-Ablaufzeit tritt auf, wenn eine Webanwendung einem Angreifer erlaubt, alte Session-Credentials oder Session-IDs zur Autorisierung wiederzuverwenden. Dies geschieht, wenn Sessions nicht nach einem angemessenen Zeitraum der Inaktivität ablaufen, bei Logout nicht ablaufen oder wenn Session-Tokens unbegrenzt gültig bleiben. Langlebige Sessions erhöhen das Zeitfenster für Session-Hijacking-Angriffe. Selbst nachdem Benutzer glauben, sich abgemeldet zu haben, können ihre Sessions für Angreifer gültig bleiben, die das Session-Token erbeutet haben.

Risiko

Langlebige Sessions erhöhen die Angriffsfläche erheblich. Gestohlene Session-Tokens bleiben über längere Zeiträume nutzbar. Geteilte oder öffentliche Computer können gültige Sessions behalten. Logout-Funktionalität, die Sessions nicht ordnungsgemäß invalidiert, gibt Benutzern falsche Sicherheit. Compliance-Frameworks erfordern Session-Timeout-Kontrollen. Session-Tokens, die in Logs, Browser-Verlauf oder durch XSS exponiert werden, bleiben ausnutzbar bis sie ablaufen. Organisationen haben Datenpannen erlitten, bei denen alte Session-Tokens Monate nach dem Diebstahl ausgenutzt wurden.

Lösung

Implementieren Sie absolute Session-Timeouts (maximale Session-Lebensdauer unabhängig von Aktivität). Implementieren Sie Idle-Timeouts (Ablauf nach Inaktivitätszeit). Invalidieren Sie Sessions serverseitig bei Logout. Regenerieren Sie Session-IDs bei Privilegänderungen (Login, Rollenwechsel). Verwenden Sie sichere Session-Speicherung mit ordnungsgemäßem Ablauf. Setzen Sie angemessene Cookie-Ablaufzeiten. Erwägen Sie Session-Binding, um Token-Wiederverwendung aus verschiedenen Kontexten zu verhindern. Für sensible Operationen fordern Sie Re-Authentifizierung.

Häufige Auswirkungen

AuswirkungDetails
ZugriffskontrolleBereich: Session-Hijacking

Langlebige Sessions erweitern das Zeitfenster für erbeutete Tokens.
VertraulichkeitBereich: Unbefugter Zugriff

Angreifer können mit alten aber noch gültigen Sessions auf Benutzerkonten zugreifen.
NichtabstreitbarkeitBereich: Zurechenbarkeit

Aktionen mit gekaperten Sessions können nicht zuverlässig zugeordnet werden.

Beispielcode + Lösungscode

Verwundbarer Code

# VERWUNDBAR: Kein Session-Timeout
from flask import Flask, session

app = Flask(__name__)
app.secret_key = 'secret'
# Keine Session-Lebensdauer konfiguriert - Sessions laufen nie ab!

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

@app.route('/logout')
def logout():
    # Löscht nur client-seitige Session-Daten
    session.clear()
    # Server verfolgt Invalidierung nicht
    # Altes Session-Cookie könnte noch funktionieren!
    return redirect('/login')

# VERWUNDBAR: Permanente Sessions
@app.route('/login', methods=['POST'])
def login_permanent():
    session.permanent = True  # Session lebt standardmäßig 31 Tage
    session['user_id'] = user.id
    # Viel zu lang für sensible Anwendungen
    return redirect('/dashboard')
// VERWUNDBAR: Kein Session-Timeout in Spring
@Configuration
public class SessionConfig {
    // Kein Session-Timeout konfiguriert
    // Sessions leben bis Server-Neustart
}

// VERWUNDBAR: Logout invalidiert Session nicht
@Controller
public class AuthController {

    @PostMapping("/logout")
    public String logout(HttpSession session) {
        // Entfernt nur Attribute, invalidiert nicht
        session.removeAttribute("user");
        // Session-ID noch gültig!
        return "redirect:/login";
    }
}

// VERWUNDBAR: Länger Session-Timeout
@Configuration
public class InsecureSessionConfig implements WebMvcConfigurer {

    @Bean
    public ServletContextInitializer servletContextInitializer() {
        return servletContext -> {
            // 7 Tage Session-Timeout - zu lang!
            servletContext.setSessionTimeout(10080);
        };
    }
}
// VERWUNDBAR: JWT ohne Ablaufzeit
const jwt = require('jsonwebtoken');

function generateToken(user) {
    // Kein Ablauf gesetzt!
    return jwt.sign({ userId: user.id }, secret);
}

// VERWUNDBAR: Langlebige Tokens
function generateTokenWithExpiry(user) {
    return jwt.sign(
        { userId: user.id },
        secret,
        { expiresIn: '365d' }  // Ein Jahr gültig!
    );
}

// VERWUNDBAR: Express-Session ohne Timeout
app.use(session({
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: true,
    cookie: {
        secure: true
        // Kein maxAge - Session-Cookie aber kein serverseitiger Ablauf!
    }
}));

// VERWUNDBAR: Logout zerstört Session nicht
app.post('/logout', (req, res) => {
    req.session.user = null;  // Setzt nur Benutzer auf null
    // Session existiert noch und könnte wiederverwendet werden
    res.redirect('/login');
});

Lösungscode

# SICHER: Ordnungsgemäße Session-Timeouts
from flask import Flask, session
from datetime import timedelta

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

# Session-Lebensdauer konfigurieren
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=30)  # Absolutes Timeout
app.config['SESSION_REFRESH_EACH_REQUEST'] = True  # Bei Aktivität zurücksetzen

# Session-Gültigkeit serverseitig verfolgen
active_sessions = {}  # In Produktion Redis verwenden

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

    # Session-ID bei Login regenerieren
    session.clear()
    session.permanent = True
    session['user_id'] = user.id
    session['created_at'] = datetime.utcnow().isoformat()
    session['session_id'] = str(uuid.uuid4())

    # Auf Server verfolgen
    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 serverseitig invalidiert
            session.clear()
            return redirect('/login')

        # Idle-Timeout prüfen (15 Minuten)
        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')

        # Absolutes Timeout prüfen (8 Stunden)
        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')

        # Letzte Aktivität aktualisieren
        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')
    # Session-Cookie löschen
    response.delete_cookie('session')
    return response

def invalidate_session(session_id):
    active_sessions.pop(session_id, None)
// SICHER: Ordnungsgemäße Session-Konfiguration
@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)  // Gleichzeitige Sessions verhindern
                .maxSessionsPreventsLogin(false)
                .expiredUrl("/login?expired")
            .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .invalidateHttpSession(true)  // Ordnungsgemäß invalidieren
                .deleteCookies("JSESSIONID")
                .clearAuthentication(true);
    }
}

// SICHER: Session-Timeout-Konfiguration
@Configuration
public class SessionConfig {

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

// SICHER: Ordnungsgemäßer Logout-Handler
@Controller
public class SecureAuthController {

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

        if (session != null) {
            // Logout-Ereignis protokollieren
            auditService.logLogout(session.getAttribute("userId"));

            // Session auf Server invalidieren
            session.invalidate();
        }

        // Cookies löschen
        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) {
        // Existierende Session invalidieren
        HttpSession oldSession = request.getSession(false);
        if (oldSession != null) {
            oldSession.invalidate();
        }

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

        // Neue Session mit neuer ID erstellen
        HttpSession newSession = request.getSession(true);
        newSession.setAttribute("userId", user.getId());
        newSession.setAttribute("createdAt", System.currentTimeMillis());
        newSession.setMaxInactiveInterval(1800);  // 30 Minuten

        return "redirect:/dashboard";
    }
}
// SICHER: JWT mit ordnungsgemäßem Ablauf und Refresh
const jwt = require('jsonwebtoken');

// Kurzlebiges Access-Token
function generateAccessToken(user) {
    return jwt.sign(
        { userId: user.id, type: 'access' },
        accessSecret,
        { expiresIn: '15m' }  // 15 Minuten
    );
}

// Längerlebiges Refresh-Token (sicher gespeichert)
function generateRefreshToken(user) {
    const token = jwt.sign(
        { userId: user.id, type: 'refresh', tokenId: uuid.v4() },
        refreshSecret,
        { expiresIn: '7d' }
    );

    // In Datenbank für Widerrufsfähigkeit speichern
    saveRefreshToken(user.id, token);

    return token;
}

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

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

        // Prüfen ob Token widerrufen wurde
        if (await isTokenRevoked(payload.tokenId)) {
            return res.status(401).json({ error: 'Token widerrufen' });
        }

        // Neues Access-Token generieren
        const accessToken = generateAccessToken({ id: payload.userId });
        res.json({ accessToken });

    } catch (err) {
        res.status(401).json({ error: 'Ungültiges Refresh-Token' });
    }
});

// SICHER: Express-Session mit ordnungsgemäßen 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,  // Ablauf bei Aktivität zurücksetzen
    cookie: {
        secure: true,
        httpOnly: true,
        sameSite: 'strict',
        maxAge: 30 * 60 * 1000  // 30 Minuten
    }
}));

// SICHER: Ordnungsgemäßer Logout
app.post('/logout', (req, res) => {
    const sessionId = req.sessionID;

    req.session.destroy(err => {
        if (err) {
            console.error('Session-Zerstörungsfehler:', err);
        }

        // Alle Refresh-Tokens widerrufen
        revokeUserRefreshTokens(req.user?.id);

        // Cookie löschen
        res.clearCookie('connect.sid', {
            path: '/',
            httpOnly: true,
            secure: true,
            sameSite: 'strict'
        });

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

// Middleware zur Session-Gültigkeitsprüfung
app.use((req, res, next) => {
    if (req.session?.userId) {
        const sessionAge = Date.now() - (req.session.createdAt || 0);
        const maxAge = 8 * 60 * 60 * 1000;  // 8 Stunden absolut

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

Ausgenutzt in der Praxis

GitHub Session-Token-Diebstahl (Mehrfach)

Langlebige GitHub Personal Access Tokens wurden von Entwicklermaschinen gestohlen und Monate später verwendet, um auf private Repositories zuzugreifen.

JWT-Token-Wiederverwendungsangriffe

Mehrere Anwendungen, die JWTs ohne Ablauf oder mit sehr langem Ablauf verwenden, haben Token-Wiederverwendungsangriffe erlitten, bei denen gestohlene Tokens unbegrenzt gültig blieben.

Corporate-VPN-Session-Persistenz

VPN-Lösungen mit unzureichender Session-Ablaufzeit haben Angreifern mit gestohlenen Session-Cookies ermöglicht, persistenten Zugriff auf Unternehmensnetzwerke zu behalten.


Tools zum Testen/Ausnutzen

  • Burp Suite — Session-Timeout-Verhalten testen
  • OWASP ZAP — Session-Management-Tests
  • JWT.io — JWT-Ablauf dekodieren und analysieren
  • Browser DevTools — Session-Cookies inspizieren

CVE-Beispiele

  • CVE-2021-22882 — UniFi Network unzureichende Session-Ablaufzeit
  • CVE-2020-8945 — GPGME Session-Token-Schwachstelle
  • CVE-2019-11358 — jQuery Session-Handling-Probleme

Referenzen

  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