Unverifizierte Passwortänderung

Beschreibung

Unverifizierte Passwortänderung tritt auf, wenn eine Anwendung Benutzern erlaubt, ihr Passwort zu ändern, ohne zu verifizieren, dass sie der legitime Kontoinhaber sind. Dies bedeutet typischerweise, dass ein neues Passwort akzeptiert wird, ohne das aktuelle Passwort zu verlangen, wobei ausschließlich auf Session-Authentifizierung vertraut wird. Während Sessions gekapert oder auf gemeinsam genutzten Geräten angemeldet gelassen werden können, stellt das Anfordern des aktuellen Passworts sicher, dass nur jemand, der es kennt, Änderungen vornehmen kann.

Risiko

Angreifer, die Sessions kapern (XSS, Session-Fixation, Netzwerkabfang), können Konten dauerhaft übernehmen, indem sie das Passwort ändern. Benutzer auf gemeinsam genutzten Computern, die sich nicht abmelden, hinterlassen verwundbare Konten. CSRF-Angriffe können Passwörter ohne Wissen des Benutzers ändern. Social-Engineering-Angriffe sind ohne Passwortverifizierung einfacher. Kontowiederherstellung wird zu dauerhafter Kompromittierung.

Lösung

Verlangen Sie immer das aktuelle Passwort, bevor Sie Passwortänderungen erlauben. Implementieren Sie Rate-Limiting bei Passwortänderungsversuchen. Senden Sie Benachrichtigungen, wenn Passwörter geändert werden. Erwägen Sie, Re-Authentifizierung für sensible Operationen zu verlangen. Implementieren Sie CSRF-Schutz auf Passwortänderungs-Formularen. Protokollieren Sie Passwortänderungs-Ereignisse für Audit-Trails.

Häufige Auswirkungen

AuswirkungDetails
AuthentifizierungBereich: Kontoübernahme

Angreifer können legitime Benutzer aussperren.
NachweisbarkeitBereich: Nicht-Abstreitbarkeit

Kontoaktionen sind möglicherweise nicht zuordenbar.
VerfügbarkeitBereich: Kontosperrung

Benutzer verlieren Zugriff auf ihre Konten.

Beispielcode + Lösungscode

Verwundbarer Code

// VERWUNDBAR: Keine Verifizierung des aktuellen Passworts
@PostMapping("/api/change-password")
public ResponseEntity<?> changePassword(
        @RequestBody PasswordChangeRequest request,
        @AuthenticationPrincipal User user) {

    // Prüft nur, ob Benutzer eingeloggt ist (Session gültig)
    // Verifiziert nicht, dass sie das aktuelle Passwort kennen!
    String newPassword = request.getNewPassword();

    // Angreifer mit gestohlener Session kann Passwort ändern
    userService.updatePassword(user.getId(), newPassword);

    return ResponseEntity.ok().build();
}

// VERWUNDBAR: CSRF-verwundbare Passwortänderung
@WebServlet("/change-password")
public class VulnerablePasswordServlet extends HttpServlet {

    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
            throws ServletException, IOException {

        HttpSession session = request.getSession();
        User user = (User) session.getAttribute("user");

        if (user != null) {
            String newPassword = request.getParameter("newPassword");
            // Keine CSRF-Token-Prüfung
            // Keine Prüfung des aktuellen Passworts
            userService.updatePassword(user.getId(), newPassword);
        }
    }
}
# VERWUNDBAR: Flask-Passwortänderung ohne Verifizierung
from flask import Flask, request, session

app = Flask(__name__)

@app.route('/change-password', methods=['POST'])
def change_password_vulnerable():
    if 'user_id' not in session:
        return 'Unauthorized', 401

    # Vertraut nur der Session - keine Passwortverifizierung
    new_password = request.form['new_password']

    user = User.query.get(session['user_id'])
    user.set_password(new_password)
    db.session.commit()

    return 'Passwort geändert'

# VERWUNDBAR: API ohne Passwortverifizierung
@app.route('/api/user/password', methods=['PUT'])
@jwt_required()
def update_password_vulnerable():
    user_id = get_jwt_identity()
    data = request.json

    # JWT gültig aber keine Passwortverifizierung
    user = User.query.get(user_id)
    user.set_password(data['new_password'])
    db.session.commit()

    return jsonify({'status': 'success'})
// VERWUNDBAR: Express.js-Passwortänderung
app.post('/change-password', requireAuth, (req, res) => {
    const userId = req.session.userId;
    const { newPassword } = req.body;

    // Nur Session-Prüfung, keine Passwortverifizierung
    User.findByIdAndUpdate(userId, {
        password: hashPassword(newPassword)
    }).then(() => {
        res.json({ success: true });
    });
});

// VERWUNDBAR: Client-seitige Passwortänderung
async function changePassword(newPassword) {
    // Kein aktuelles Passwort gesendet
    const response = await fetch('/api/password', {
        method: 'PUT',
        headers: {
            'Authorization': `Bearer ${getToken()}`,
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({ newPassword })
    });

    return response.json();
}

Lösungscode

// SICHER: Aktuelles Passwort für Änderung erforderlich
@PostMapping("/api/change-password")
public ResponseEntity<?> changePassword(
        @RequestBody PasswordChangeRequest request,
        @AuthenticationPrincipal User user) {

    // Aktuelles Passwort verifizieren
    if (!passwordEncoder.matches(request.getCurrentPassword(),
                                  user.getPassword())) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
            .body("Aktuelles Passwort ist falsch");
    }

    // Neues Passwort validieren
    ValidationResult validation = passwordValidator.validate(
        request.getNewPassword()
    );
    if (!validation.isValid()) {
        return ResponseEntity.badRequest()
            .body(validation.getErrors());
    }

    // Passwort aktualisieren
    userService.updatePassword(user.getId(), request.getNewPassword());

    // Benachrichtigung senden
    notificationService.sendPasswordChangeNotification(user);

    // Andere Sessions invalidieren
    sessionService.invalidateOtherSessions(user.getId());

    return ResponseEntity.ok().build();
}

// SICHER: Mit CSRF-Schutz
@WebServlet("/change-password")
public class SafePasswordServlet extends HttpServlet {

    @Inject
    private CsrfTokenService csrfService;

    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
            throws ServletException, IOException {

        // CSRF-Token verifizieren
        if (!csrfService.validateToken(request)) {
            response.sendError(403, "Ungültiges CSRF-Token");
            return;
        }

        HttpSession session = request.getSession();
        User user = (User) session.getAttribute("user");

        if (user == null) {
            response.sendError(401);
            return;
        }

        String currentPassword = request.getParameter("currentPassword");
        String newPassword = request.getParameter("newPassword");

        // Aktuelles Passwort verifizieren
        if (!authService.verifyPassword(user, currentPassword)) {
            response.sendError(403, "Aktuelles Passwort falsch");
            return;
        }

        // Passwort ändern
        userService.updatePassword(user.getId(), newPassword);

        // Audit-Log
        auditService.log("PASSWORD_CHANGE", user.getId());
    }
}
# SICHER: Flask mit Passwortverifizierung
from flask import Flask, request, session, jsonify
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)
csrf = CSRFProtect(app)

@app.route('/change-password', methods=['POST'])
def change_password_safe():
    if 'user_id' not in session:
        return jsonify({'error': 'Nicht autorisiert'}), 401

    current_password = request.form['current_password']
    new_password = request.form['new_password']

    user = User.query.get(session['user_id'])

    # Aktuelles Passwort verifizieren
    if not user.check_password(current_password):
        return jsonify({'error': 'Aktuelles Passwort falsch'}), 403

    # Neues Passwort validieren
    if not is_strong_password(new_password):
        return jsonify({'error': 'Passwort zu schwach'}), 400

    # Passwort aktualisieren
    user.set_password(new_password)
    db.session.commit()

    # Benachrichtigungs-E-Mail senden
    send_password_change_notification(user.email)

    # Andere Sessions invalidieren
    invalidate_other_sessions(user.id)

    return jsonify({'status': 'success'})

# SICHER: API mit Re-Authentifizierung
@app.route('/api/user/password', methods=['PUT'])
@jwt_required()
def update_password_safe():
    user_id = get_jwt_identity()
    data = request.json

    if 'current_password' not in data:
        return jsonify({'error': 'Aktuelles Passwort erforderlich'}), 400

    user = User.query.get(user_id)

    # Aktuelles Passwort verifizieren
    if not user.check_password(data['current_password']):
        # Fehlgeschlagene Versuche begrenzen
        record_failed_attempt(user_id)
        return jsonify({'error': 'Ungültiges Passwort'}), 403

    # Rate-Limiting prüfen
    if is_rate_limited(user_id):
        return jsonify({'error': 'Zu viele Versuche'}), 429

    # Passwort aktualisieren
    user.set_password(data['new_password'])
    db.session.commit()

    # Audit-Log
    log_security_event('password_changed', user_id)

    return jsonify({'status': 'success'})
// SICHER: Express.js mit Passwortverifizierung
const rateLimit = require('express-rate-limit');

const passwordChangeLimit = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 5
});

app.post('/change-password',
         requireAuth,
         csrf.protect,
         passwordChangeLimit,
         async (req, res) => {

    const userId = req.session.userId;
    const { currentPassword, newPassword } = req.body;

    // Aktuelles Passwort erforderlich
    if (!currentPassword) {
        return res.status(400).json({
            error: 'Aktuelles Passwort erforderlich'
        });
    }

    const user = await User.findById(userId);

    // Aktuelles Passwort verifizieren
    const isValid = await bcrypt.compare(currentPassword, user.password);
    if (!isValid) {
        return res.status(403).json({
            error: 'Aktuelles Passwort falsch'
        });
    }

    // Neues Passwort validieren
    const validation = validatePassword(newPassword);
    if (!validation.valid) {
        return res.status(400).json({
            error: validation.message
        });
    }

    // Passwort aktualisieren
    user.password = await bcrypt.hash(newPassword, 10);
    await user.save();

    // Benachrichtigung senden
    await sendEmail(user.email, 'Passwort geändert',
                   'Ihr Passwort wurde geändert.');

    // Ereignis protokollieren
    await AuditLog.create({
        userId,
        action: 'password_change',
        ip: req.ip
    });

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

// SICHER: Client-seitiges Formular
<form action="/change-password" method="POST">
    <input type="hidden" name="_csrf" value="{{csrfToken}}">

    <label>Aktuelles Passwort:
        <input type="password" name="currentPassword" required>
    </label>

    <label>Neues Passwort:
        <input type="password" name="newPassword" required
               minlength="8" pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}">
    </label>

    <label>Neues Passwort bestätigen:
        <input type="password" name="confirmPassword" required>
    </label>

    <button type="submit">Passwort ändern</button>
</form>

Ausgenutzt in der Praxis

Session-Hijacking zur Kontoübernahme

XSS-Angriffe, die Sessions stehlen, führten zu dauerhafter Kontokompromittierung über Passwortänderung.

CSRF-Passwortänderungen

Bösartige Websites änderten Passwörter von eingeloggten Benutzern, die sie besuchten.

Ausnutzung gemeinsam genutzter Computer

Konten auf Bibliotheks-Computern wurden über Passwortänderung übernommen.


Tools zum Testen/Ausnutzen

  • Burp Suite — Passwortänderungs-Flows testen.
  • CSRF-Testtools.
  • Session-Hijacking-Demonstrationen.

CVE-Beispiele

  • CVEs durch Passwortänderung ohne Verifizierung.
  • CSRF-Angriffe auf Passwortänderungs-Endpunkte.

Referenzen

  1. MITRE. "CWE-620: Unverified Password Change." https://cwe.mitre.org/data/definitions/620.html
  2. OWASP. "Forgot Password Cheat Sheet." https://cheatsheetseries.owasp.org/