Übermäßig restriktiver Kontosperrmechanismus
Beschreibung
Übermäßig restriktiver Kontosperrmechanismus tritt auf, wenn die Kontosperrrichtlinie einer Anwendung zu aggressiv ist und Angreifern ermöglicht, legitime Benutzer absichtlich auszusperren. Durch wiederholtes Eingeben falscher Passwörter für gezielt ausgewählte Konten können Angreifer Denial of Service gegen bestimmte Benutzer verursachen. Dies ist besonders schwerwiegend in Kombination mit Benutzeraufzählungs-Schwachstellen.
Risiko
Angreifer können jedes Benutzerkonto durch wiederholte fehlgeschlagene Login-Versuche sperren. Gezielte Denial of Service gegen Administratoren oder kritische Benutzer. In Kombination mit Benutzeraufzählung ermöglicht dies systematische Sperrung aller Benutzer. Geschäftsunterbrechung, wenn Schlüsselpersonal nicht auf Systeme zugreifen kann. Support-Kosten steigen durch sperrungsbezogene Tickets. Kann verwendet werden, um andere Angriffe zu verschleiern, indem Sicherheitspersonal ausgesperrt wird.
Lösung
Implementieren Sie progressive Verzögerungen (exponentielles Backoff) anstelle von harten Sperrungen. Verwenden Sie CAPTCHA nach fehlgeschlagenen Versuchen. Sperren Sie nach IP-Adresse zusätzlich zu oder anstelle von Benutzernamen. Erfordern Sie sekundäre Verifizierung für Entsperrung anstelle von zeitbasierter Entsperrung. Implementieren Sie Benachrichtigung bei wiederholten fehlgeschlagenen Versuchen. Verwenden Sie Multi-Faktor-Authentifizierung, um den Wert von Passwort-Raten zu reduzieren.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Verfügbarkeit | Bereich: Benutzersperrung Legitimen Benutzern wird Zugang verweigert. |
| Geschäft | Bereich: Unterbrechung Kritische Operationen gestoppt. |
| Sicherheit | Bereich: Angriff verschleiern Sperrungen maskieren andere bösartige Aktivitäten. |
Beispielcode + Lösungscode
Verwundbarer Code
// VERWUNDBAR: Harte Sperrung nach fehlgeschlagenen Versuchen
public class VulnerableLockoutAuth {
private static final int MAX_ATTEMPTS = 3;
private Map<String, Integer> failedAttempts = new ConcurrentHashMap<>();
private Set<String> lockedAccounts = ConcurrentHashMap.newKeySet();
public AuthResult authenticate(String username, String password) {
// Prüfen ob gesperrt
if (lockedAccounts.contains(username)) {
return AuthResult.accountLocked();
}
User user = userRepository.findByUsername(username);
if (user == null || !verifyPassword(password, user.getPasswordHash())) {
int attempts = failedAttempts.merge(username, 1, Integer::sum);
if (attempts >= MAX_ATTEMPTS) {
// VERWUNDBAR: Harte Sperrung - jeder Angreifer kann jedes Konto sperren
lockedAccounts.add(username);
log.info("Konto gesperrt: " + username);
}
return AuthResult.invalidCredentials();
}
failedAttempts.remove(username);
return AuthResult.success(user);
}
// Nur Admin kann entsperren - erzeugt Support-Belastung
public void unlockAccount(String username) {
lockedAccounts.remove(username);
failedAttempts.remove(username);
}
}
// VERWUNDBAR: Zeitbasierte Sperrung ermöglicht dennoch DoS
public class VulnerableTimeLockout {
private static final int LOCKOUT_DURATION_MS = 30 * 60 * 1000; // 30 Minuten
private Map<String, Long> lockoutEnd = new ConcurrentHashMap<>();
public AuthResult authenticate(String username, String password) {
Long lockoutEndTime = lockoutEnd.get(username);
if (lockoutEndTime != null && System.currentTimeMillis() < lockoutEndTime) {
// VERWUNDBAR: Angreifer schlägt wiederholt fehl, um Sperrung zu verlängern
return AuthResult.accountLocked(getRemainingTime(lockoutEndTime));
}
// ... Authentifizierungslogik
if (failed) {
if (getFailedAttempts(username) >= 3) {
// VERWUNDBAR: Angreifer löst Sperrung wiederholt aus
lockoutEnd.put(username, System.currentTimeMillis() + LOCKOUT_DURATION_MS);
}
}
}
}
# VERWUNDBAR: Einfacher Sperrmechanismus
from datetime import datetime, timedelta
class VulnerableLockout:
def __init__(self):
self.failed_attempts = {}
self.locked_until = {}
self.MAX_ATTEMPTS = 5
self.LOCKOUT_MINUTES = 30
def authenticate(self, username, password):
# Sperrung prüfen
if username in self.locked_until:
if datetime.now() < self.locked_until[username]:
return {'error': 'Konto gesperrt'}
else:
del self.locked_until[username]
self.failed_attempts[username] = 0
user = get_user(username)
if not user or not verify_password(password, user.password_hash):
# Fehlversuche inkrementieren
self.failed_attempts[username] = self.failed_attempts.get(username, 0) + 1
if self.failed_attempts[username] >= self.MAX_ATTEMPTS:
# VERWUNDBAR: Jeder Angreifer kann jedes Konto sperren
self.locked_until[username] = datetime.now() + timedelta(minutes=self.LOCKOUT_MINUTES)
return {'error': 'Konto wegen zu vieler Versuche gesperrt'}
return {'error': 'Ungültige Anmeldedaten'}
self.failed_attempts[username] = 0
return {'success': True, 'user': user}
# VERWUNDBAR: Globale Sperrung ohne IP-Tracking
class GlobalVulnerableLockout:
def __init__(self):
self.lockout_count = {}
def check_lockout(self, username):
# Prüft nur Benutzernamen, nicht IP
# Angreifer von jeder IP kann Konto sperren
count = self.lockout_count.get(username, 0)
if count >= 3:
return True
return False
def record_failure(self, username):
# VERWUNDBAR: Kein IP-Tracking
self.lockout_count[username] = self.lockout_count.get(username, 0) + 1
// VERWUNDBAR: Express.js harte Sperrung
const lockedAccounts = new Map();
const failedAttempts = new Map();
const MAX_ATTEMPTS = 3;
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Prüfen ob gesperrt
if (lockedAccounts.has(username)) {
return res.status(403).json({ error: 'Konto gesperrt' });
}
const user = await db.findUser(username);
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
const attempts = (failedAttempts.get(username) || 0) + 1;
failedAttempts.set(username, attempts);
if (attempts >= MAX_ATTEMPTS) {
// VERWUNDBAR: Harte Sperrung
lockedAccounts.set(username, true);
return res.status(403).json({ error: 'Konto gesperrt' });
}
return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
}
failedAttempts.delete(username);
// ... Login erfolgreich
});
// VERWUNDBAR: Permanente Sperrung bis manuelle Zurücksetzung
app.post('/admin/unlock', requireAdmin, (req, res) => {
const { username } = req.body;
lockedAccounts.delete(username);
failedAttempts.delete(username);
res.json({ success: true });
// Jede Sperrung erfordert Admin-Eingriff!
});
Lösungscode
// SICHER: Multifaktor-Sperrung mit progressiven Verzögerungen
public class SafeLockoutAuth {
private final Cache<String, AttemptRecord> attemptCache;
private final Cache<String, AttemptRecord> ipAttemptCache;
public SafeLockoutAuth() {
// Kurzlebiger Cache für Versuchs-Tracking
this.attemptCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
this.ipAttemptCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
}
public AuthResult authenticate(String username, String password, String ipAddress) {
AttemptRecord userRecord = attemptCache.getIfPresent(username);
AttemptRecord ipRecord = ipAttemptCache.getIfPresent(ipAddress);
// IP-basierte Rate-Limitierung zuerst prüfen
if (ipRecord != null && ipRecord.isRateLimited()) {
return AuthResult.rateLimited(ipRecord.getRetryAfter());
}
// Progressive Verzögerung für Benutzerkonto
if (userRecord != null && userRecord.isDelayed()) {
// Verzögerungszeit zurückgeben aber nicht sperren
return AuthResult.pleaseWait(userRecord.getRequiredDelay());
}
User user = userRepository.findByUsername(username);
if (user == null || !verifyPassword(password, user.getPasswordHash())) {
recordFailure(username, ipAddress);
return AuthResult.invalidCredentials();
}
clearFailures(username, ipAddress);
return AuthResult.success(user);
}
private void recordFailure(String username, String ipAddress) {
// Nach Benutzername mit progressiver Verzögerung aufzeichnen
AttemptRecord userRecord = attemptCache.getIfPresent(username);
if (userRecord == null) {
userRecord = new AttemptRecord();
}
userRecord.increment();
// Exponentielles Backoff: 1s, 2s, 4s, 8s, 16s...
userRecord.setDelay((long) Math.pow(2, Math.min(userRecord.getAttempts() - 1, 6)) * 1000);
attemptCache.put(username, userRecord);
// Nach IP mit härteren Limits aufzeichnen
AttemptRecord ipRecord = ipAttemptCache.getIfPresent(ipAddress);
if (ipRecord == null) {
ipRecord = new AttemptRecord();
}
ipRecord.increment();
// IP wird nach vielen Versuchen rate-limited
if (ipRecord.getAttempts() >= 20) {
ipRecord.setRateLimited(true);
ipRecord.setRetryAfter(System.currentTimeMillis() + 300000); // 5 Minuten
}
ipAttemptCache.put(ipAddress, ipRecord);
}
}
// SICHER: CAPTCHA-basierter Schutz
public class CaptchaProtectedAuth {
public AuthResult authenticate(String username, String password,
String captchaResponse, String ipAddress) {
int attempts = getAttemptCount(username, ipAddress);
// CAPTCHA nach 3 Versuchen erfordern
if (attempts >= 3) {
if (!verifyCaptcha(captchaResponse)) {
return AuthResult.captchaRequired();
}
}
// Normaler Auth-Ablauf...
}
}
# SICHER: Progressive Verzögerung mit IP-Tracking
from datetime import datetime, timedelta
from functools import lru_cache
import redis
class SafeLockout:
def __init__(self, redis_client):
self.redis = redis_client
def get_delay_seconds(self, attempts):
"""Exponentielles Backoff: 1, 2, 4, 8, 16, 32, 60 Sekunden max"""
if attempts < 1:
return 0
return min(2 ** (attempts - 1), 60)
def authenticate(self, username, password, ip_address):
# IP-Rate-Limit prüfen
ip_key = f"auth:ip:{ip_address}"
ip_attempts = int(self.redis.get(ip_key) or 0)
if ip_attempts >= 50: # Hohe Schwelle für IP
return {'error': 'Zu viele Anfragen von dieser IP', 'retry_after': 300}
# Benutzerversuche holen
user_key = f"auth:user:{username}"
user_attempts = int(self.redis.get(user_key) or 0)
# Prüfen ob Verzögerung erforderlich (keine Sperrung!)
delay = self.get_delay_seconds(user_attempts)
last_attempt_key = f"auth:last:{username}"
last_attempt = self.redis.get(last_attempt_key)
if last_attempt:
elapsed = (datetime.now() - datetime.fromisoformat(last_attempt.decode())).total_seconds()
if elapsed < delay:
remaining = delay - elapsed
return {'error': 'Bitte warten', 'retry_after': remaining}
# Authentifizieren
user = get_user(username)
if not user or not verify_password(password, user.password_hash):
# Fehlschlag aufzeichnen
self.redis.incr(user_key)
self.redis.expire(user_key, 3600) # 1 Stunde Ablauf
self.redis.set(last_attempt_key, datetime.now().isoformat(), ex=3600)
self.redis.incr(ip_key)
self.redis.expire(ip_key, 3600)
return {'error': 'Ungültige Anmeldedaten'}
# Erfolg - Benutzerversuche löschen (nicht IP)
self.redis.delete(user_key)
self.redis.delete(last_attempt_key)
return {'success': True, 'user': user}
# SICHER: Mit CAPTCHA-Unterstützung
class CaptchaProtectedAuth:
def __init__(self, redis_client, captcha_service):
self.redis = redis_client
self.captcha_service = captcha_service
def authenticate(self, username, password, ip_address, captcha_token=None):
user_key = f"auth:attempts:{username}"
attempts = int(self.redis.get(user_key) or 0)
# CAPTCHA nach 3 Versuchen erfordern
if attempts >= 3:
if not captcha_token:
return {'error': 'Captcha erforderlich', 'require_captcha': True}
if not self.captcha_service.verify(captcha_token):
return {'error': 'Ungültiges Captcha', 'require_captcha': True}
# Normale Authentifizierung...
// SICHER: Express mit progressiven Verzögerungen und IP-Tracking
const rateLimit = require('express-rate-limit');
const redis = require('redis');
const client = redis.createClient();
// IP-basierte Rate-Limitierung
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 Stunde
max: 100, // 100 Anfragen pro IP pro Stunde
message: { error: 'Zu viele Anfragen von dieser IP' }
});
function getDelaySeconds(attempts) {
if (attempts < 1) return 0;
return Math.min(Math.pow(2, attempts - 1), 60);
}
app.post('/login', loginLimiter, async (req, res) => {
const { username, password, captchaToken } = req.body;
const ip = req.ip;
// Versuchszähler für Benutzer holen
const attempts = parseInt(await client.get(`auth:${username}`) || 0);
// Progressive Verzögerung prüfen
const delay = getDelaySeconds(attempts);
const lastAttempt = await client.get(`auth:last:${username}`);
if (lastAttempt) {
const elapsed = (Date.now() - parseInt(lastAttempt)) / 1000;
if (elapsed < delay) {
return res.status(429).json({
error: 'Bitte warten',
retryAfter: Math.ceil(delay - elapsed)
});
}
}
// CAPTCHA nach 3 Versuchen erfordern
if (attempts >= 3) {
if (!captchaToken || !await verifyCaptcha(captchaToken)) {
return res.status(400).json({
error: 'Captcha erforderlich',
requireCaptcha: true
});
}
}
// Authentifizieren
const user = await db.findUser(username);
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
// Fehlschlag mit TTL aufzeichnen
await client.incr(`auth:${username}`);
await client.expire(`auth:${username}`, 3600);
await client.set(`auth:last:${username}`, Date.now(), 'EX', 3600);
const newAttempts = attempts + 1;
return res.status(401).json({
error: 'Ungültige Anmeldedaten',
requireCaptcha: newAttempts >= 3,
retryAfter: getDelaySeconds(newAttempts)
});
}
// Bei Erfolg löschen
await client.del(`auth:${username}`);
await client.del(`auth:last:${username}`);
// ... Login erfolgreich
});
Ausgenutzt in der Praxis
Kontosperrungs-Angriffe
Gezielte Sperrung von Admin-Konten während Angriffen.
Massen-Sperrung-DoS
Skriptgesteuerte Sperrung ganzer Benutzerbasen.
Wettbewerbsangriffe
Sperren von Geschäftskonten von Konkurrenten.
Tools zum Testen/Ausnutzen
- Hydra/Medusa für automatisierte Login-Versuche.
- Benutzerdefinierte Skripte für gezielte Sperrungen.
- Lasttest-Tools.
CVE-Beispiele
- Kontosperrungs-DoS-Schwachstellen in verschiedenen Anwendungen.
- Geschäftsauswirkungen durch übermäßige Sperrrichtlinien.
Referenzen
- MITRE. "CWE-645: Overly Restrictive Account Lockout Mechanism." https://cwe.mitre.org/data/definitions/645.html
- OWASP. "Authentication Cheat Sheet."