Schwache Passwortanforderungen

Beschreibung

Schwache Passwortanforderungen ist eine Schwachstelle, bei der ein Produkt nicht verlangt, dass Benutzer starke Passwörter haben, wodurch Angreifer Anmeldedaten leicht erraten können. Authentifizierungssysteme verlassen sich auf gemerkte Geheimnisse (Passwörter), um die Benutzeridentität zu verifizieren, und die Passwortkomplexität muss ausreichend sein, um zu verhindern, dass Angreifer Anmeldedaten durch Brute-Force- oder Wörterbuchangriffe erraten. Schwache Anforderungen wie das Erlauben kurzer Passwörter, kein Erfordernis von Zeichenvielfalt, das Zulassen gängiger Wörterbuchbegriffe oder das Nicht-Blockieren kontextueller Zeichenfolgen machen es Angreifern erheblich einfacher, Benutzerkonten zu kompromittieren.

Risiko

Schwache Passwortanforderungen ermöglichen direkt anmeldedatenbasierte Angriffe. Kurze Passwörter können je nach Länge in Sekunden bis Stunden per Brute-Force geknackt werden. Passwörter ohne Komplexitätsanforderungen sind anfällig für Wörterbuchangriffe mit gängigen Wortlisten. Das Erlauben von Benutzernamen, Anwendungsnamen oder gängigen Phrasen in Passwörtern macht Konten trivial kompromittierbar. Ohne Einschränkungen zur Passwortwiederverwendung sind Credential-Stuffing-Angriffe mit geleakten Datenbanken hochgradig erfolgreich. Schwache Passwörter auf Administratorkonten können zur vollständigen Systemkompromittierung führen. Der kumulative Effekt ist, dass Angreifer unbefugten Zugriff auf Benutzerkonten erlangen und möglicherweise zu systemweiten Breaches eskalieren können.

Lösung

Implementieren Sie umfassende Passwortanforderungen: Erzwingen Sie Mindestlänge (mindestens 8 Zeichen, vorzugsweise 12+), Maximallänge mit Erlaubnis für Passphrasen und blockieren Sie gängige Wörterbuch-Passwörter. Beschränken Sie kontextuelle Zeichenfolgen wie Benutzernamen oder Anwendungsnamen. Erwägen Sie das Erfordernis gemischter Zeichensätze, aber priorisieren Sie Länge über Komplexität für bessere Sicherheit und Benutzerfreundlichkeit. Implementieren Sie Passwortstärke-Anzeigen, um Benutzer zu führen. Verwenden Sie bcrypt, Argon2 oder PBKDF2 für Passwort-Hashing. Fügen Sie Multi-Faktor-Authentifizierung als Defense-in-Depth hinzu. Erwägen Sie Passphrasen-basierte Ansätze, die langere, leichter merkbare Passwörter fördern. Beachten Sie, dass erzwungene periodische Passwortänderungen nicht mehr empfohlen werden, da sie oft zu schwächeren Passwörtern führen.

Häufige Auswirkungen

AuswirkungDetails
ZugriffskontrolleBereich: Zugriffskontrolle

Privilegien erlangen oder Identität annehmen - Ein Angreifer könnte Benutzerpasswörter leicht erraten und unbefugten Zugriff auf Konten erlangen, möglicherweise einschließlich Administratorkonten mit erhöhten Privilegien.
VertraulichkeitBereich: Vertraulichkeit

Anwendungsdaten lesen - Kompromittierte Konten können Zugriff auf sensible Benutzerdaten, persönliche Informationen oder vertrauliche Geschäftsinformationen bieten.

Beispielcode

Verwundbarer Code

// Verwundbar: Keine Passwortstärke-Validierung
public class VulnerableAuthService {

    // Verwundbar: Keine Mindestlängenanforderung
    public boolean registerUser(String username, String password) {
        // Verwundbar: Akzeptiert jedes Passwort
        if (password != null && !password.isEmpty()) {
            User user = new User(username, hashPassword(password));
            userRepository.save(user);
            return true;
        }
        return false;
    }

    // Verwundbar: Schwacher Passwort-Hash
    private String hashPassword(String password) {
        return DigestUtils.md5Hex(password);  // MD5 ist unsicher
    }
}

// Verwundbar: Minimale Validierung, die leicht umgangen werden kann
public class WeakPasswordValidator {

    // Verwundbar: Prüft nur Länge
    public boolean validatePassword(String password) {
        return password != null && password.length() >= 4;  // Zu kurz!
    }

    // Verwundbar: Einfache Komplexitätsprüfung leicht erfüllbar
    public boolean hasUpperAndLower(String password) {
        return password.matches(".*[A-Z].*") &&
               password.matches(".*[a-z].*");
        // "Aa" würde bestehen - nicht sicher
    }
}
# Verwundbar: Python-Registrierung mit schwachen Anforderungen
from flask import Flask, request
import hashlib

app = Flask(__name__)

@app.route('/register', methods=['POST'])
def vulnerable_register():
    username = request.form['username']
    password = request.form['password']

    # Verwundbar: Keine Längenprüfung
    # Verwundbar: Keine Komplexitätsanforderungen
    # Verwundbar: Keine Prüfung auf gängige Passwörter

    # Verwundbar: Schwacher Hash
    password_hash = hashlib.md5(password.encode()).hexdigest()

    save_user(username, password_hash)
    return "Benutzer registriert"

# Verwundbar: Akzeptiert extrem schwache Passwörter
def validate_password_weak(password):
    # Verwundbar: Prüft nur auf nicht-leer
    return bool(password)

# Verwundbar: Trivial erfüllbare Anforderungen
def validate_password_minimal(password):
    # Verwundbar: 6 Zeichen sind zu kurz
    if len(password) < 6:
        return False
    # Keine weiteren Prüfungen - "aaaaaa" würde bestehen
    return True
// Verwundbar: Nur clientseitige Validierung
function vulnerableValidatePassword(password) {
    // Verwundbar: Keine serverseitige Validierung
    // Angreifer kann durch direkten API-Aufruf umgehen

    // Verwundbar: Schwache Anforderungen
    if (password.length >= 4) {
        return true;
    }
    return false;
}

// Verwundbar: Express.js Registrierungs-Endpunkt
app.post('/register', (req, res) => {
    const { username, password } = req.body;

    // Verwundbar: Keine Passwortvalidierung
    // Verwundbar: Erlaubt "1234", "password", "admin", etc.

    const user = createUser(username, password);
    res.json({ success: true });
});

Lösungscode

// Behoben: Umfassende Passwortvalidierung
public class SecurePasswordValidator {

    private static final int MIN_LENGTH = 12;
    private static final int MAX_LENGTH = 128;
    private static final Set<String> COMMON_PASSWORDS = loadCommonPasswords();

    public ValidationResult validatePassword(String password, String username) {
        List<String> errors = new ArrayList<>();

        // Behoben: Null/Leer prüfen
        if (password == null || password.isEmpty()) {
            return ValidationResult.invalid("Passwort ist erforderlich");
        }

        // Behoben: Mindestlänge erzwingen
        if (password.length() < MIN_LENGTH) {
            errors.add("Passwort muss mindestens " + MIN_LENGTH + " Zeichen haben");
        }

        // Behoben: Maximallänge erzwingen (DoS bei Hashing verhindern)
        if (password.length() > MAX_LENGTH) {
            errors.add("Passwort darf " + MAX_LENGTH + " Zeichen nicht überschreiten");
        }

        // Behoben: Gängige Passwörter blockieren
        if (COMMON_PASSWORDS.contains(password.toLowerCase())) {
            errors.add("Dieses Passwort ist zu gebräuchlich");
        }

        // Behoben: Kontextuelle Zeichenfolgen blockieren
        if (username != null && password.toLowerCase()
                .contains(username.toLowerCase())) {
            errors.add("Passwort darf Ihren Benutzernamen nicht enthalten");
        }

        // Behoben: Zeichenvielfalt prüfen (optional, Länge bevorzugen)
        if (!hasCharacterVariety(password)) {
            errors.add("Passwort sollte eine Mischung von Zeichentypen enthalten");
        }

        // Behoben: Gegen geleakte Passwörter prüfen (optional)
        if (isBreachedPassword(password)) {
            errors.add("Dieses Passwort ist in Datenlecks aufgetaucht");
        }

        return errors.isEmpty() ?
            ValidationResult.valid() :
            ValidationResult.invalid(errors);
    }

    private boolean hasCharacterVariety(String password) {
        boolean hasLower = password.matches(".*[a-z].*");
        boolean hasUpper = password.matches(".*[A-Z].*");
        boolean hasDigit = password.matches(".*\\d.*");
        boolean hasSpecial = password.matches(".*[!@#$%^&*(),.?\":{}|<>].*");

        // Mindestens 3 von 4 Zeichentypen erfordern
        int count = 0;
        if (hasLower) count++;
        if (hasUpper) count++;
        if (hasDigit) count++;
        if (hasSpecial) count++;

        return count >= 3;
    }

    private boolean isBreachedPassword(String password) {
        // HaveIBeenPwned k-Anonymity API verwenden
        return PwnedPasswordsService.isCompromised(password);
    }

    private static Set<String> loadCommonPasswords() {
        // Top 10000 gängige Passwörter laden
        return CommonPasswordLoader.load("/common-passwords.txt");
    }
}

// Behoben: Sicheres Passwort-Hashing
public class SecureAuthService {

    private static final int BCRYPT_COST = 12;

    public boolean registerUser(String username, String password) {
        // Behoben: Passwortstärke validieren
        ValidationResult result = passwordValidator
            .validatePassword(password, username);

        if (!result.isValid()) {
            throw new WeakPasswordException(result.getErrors());
        }

        // Behoben: Starkes Passwort-Hashing verwenden
        String hash = BCrypt.hashpw(password, BCrypt.gensalt(BCRYPT_COST));

        User user = new User(username, hash);
        userRepository.save(user);

        return true;
    }

    public boolean verifyPassword(String username, String password) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            // Behoben: Timing-Angriffe verhindern
            BCrypt.hashpw(password, BCrypt.gensalt(BCRYPT_COST));
            return false;
        }
        return BCrypt.checkpw(password, user.getPasswordHash());
    }
}
# Behoben: Sichere Passwortvalidierung in Python
import re
import bcrypt
from typing import List, Tuple
import requests

class SecurePasswordValidator:
    MIN_LENGTH = 12
    MAX_LENGTH = 128

    def __init__(self):
        self.common_passwords = self._load_common_passwords()

    def validate(self, password: str, username: str = None) -> Tuple[bool, List[str]]:
        errors = []

        if not password:
            return False, ["Passwort ist erforderlich"]

        # Behoben: Längenanforderungen erzwingen
        if len(password) < self.MIN_LENGTH:
            errors.append(f"Passwort muss mindestens {self.MIN_LENGTH} Zeichen haben")

        if len(password) > self.MAX_LENGTH:
            errors.append(f"Passwort darf {self.MAX_LENGTH} Zeichen nicht überschreiten")

        # Behoben: Gängige Passwörter blockieren
        if password.lower() in self.common_passwords:
            errors.append("Dieses Passwort ist zu gebräuchlich")

        # Behoben: Benutzername in Passwort blockieren
        if username and username.lower() in password.lower():
            errors.append("Passwort darf Ihren Benutzernamen nicht enthalten")

        # Behoben: Zeichenvielfalt prüfen
        if not self._has_variety(password):
            errors.append("Passwort sollte gemischte Zeichen enthalten")

        # Behoben: Gegen geleakte Passwörter prüfen
        if self._is_breached(password):
            errors.append("Dieses Passwort ist in Datenlecks aufgetaucht")

        return len(errors) == 0, errors

    def _has_variety(self, password: str) -> bool:
        checks = [
            re.search(r'[a-z]', password),  # Kleinbuchstaben
            re.search(r'[A-Z]', password),  # Großbuchstaben
            re.search(r'\d', password),      # Ziffer
            re.search(r'[!@#$%^&*(),.?":{}|<>]', password)  # Sonderzeichen
        ]
        return sum(bool(c) for c in checks) >= 3

    def _is_breached(self, password: str) -> bool:
        # HaveIBeenPwned k-Anonymity API verwenden
        import hashlib
        sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
        prefix, suffix = sha1[:5], sha1[5:]

        response = requests.get(f'https://api.pwnedpasswords.com/range/{prefix}')
        if response.status_code == 200:
            hashes = dict(line.split(':') for line in response.text.splitlines())
            return suffix in hashes
        return False

    def _load_common_passwords(self) -> set:
        # Gängige Passwortliste laden
        with open('common-passwords.txt', 'r') as f:
            return set(line.strip().lower() for line in f)


# Behoben: Sicherer Registrierungs-Endpunkt
from flask import Flask, request, jsonify

app = Flask(__name__)
password_validator = SecurePasswordValidator()

@app.route('/register', methods=['POST'])
def secure_register():
    username = request.form.get('username')
    password = request.form.get('password')

    # Behoben: Serverseitige Validierung
    is_valid, errors = password_validator.validate(password, username)

    if not is_valid:
        return jsonify({'errors': errors}), 400

    # Behoben: Starkes Passwort-Hashing
    password_hash = bcrypt.hashpw(
        password.encode(),
        bcrypt.gensalt(rounds=12)
    )

    save_user(username, password_hash)
    return jsonify({'message': 'Benutzer erfolgreich registriert'})

CVE-Beispiele

  • CVE-2020-4574: IBM Key Management Server erfordert standardmäßig keine starken Passwörter, was Angreifern ermöglicht, Benutzerkonten durch schwache Anmeldedaten leichter zu kompromittieren.

Referenzen

  1. MITRE Corporation. "CWE-521: Weak Password Requirements." https://cwe.mitre.org/data/definitions/521.html
  2. NIST SP 800-63B. "Digital Identity Guidelines: Authentication and Lifecycle Management."
  3. OWASP. "Authentication Cheat Sheet."