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
| Auswirkung | Details |
|---|---|
| Zugriffskontrolle | Bereich: Session-Hijacking Langlebige Sessions erweitern das Zeitfenster für erbeutete Tokens. |
| Vertraulichkeit | Bereich: Unbefugter Zugriff Angreifer können mit alten aber noch gültigen Sessions auf Benutzerkonten zugreifen. |
| Nichtabstreitbarkeit | Bereich: 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
- MITRE. "CWE-613: Insufficient Session Expiration." https://cwe.mitre.org/data/definitions/613.html
- OWASP. "Session Management Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html