Ü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

AuswirkungDetails
VerfügbarkeitBereich: Benutzersperrung

Legitimen Benutzern wird Zugang verweigert.
GeschäftBereich: Unterbrechung

Kritische Operationen gestoppt.
SicherheitBereich: 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

  1. MITRE. "CWE-645: Overly Restrictive Account Lockout Mechanism." https://cwe.mitre.org/data/definitions/645.html
  2. OWASP. "Authentication Cheat Sheet."