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
| Auswirkung | Details |
|---|---|
| Authentifizierung | Bereich: Kontoübernahme Angreifer können legitime Benutzer aussperren. |
| Nachweisbarkeit | Bereich: Nicht-Abstreitbarkeit Kontoaktionen sind möglicherweise nicht zuordenbar. |
| Verfügbarkeit | Bereich: 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
- MITRE. "CWE-620: Unverified Password Change." https://cwe.mitre.org/data/definitions/620.html
- OWASP. "Forgot Password Cheat Sheet." https://cheatsheetseries.owasp.org/