Unzureichende Durchsetzung des Verhaltens-Workflows
Beschreibung
Unzureichende Durchsetzung des Verhaltens-Workflows ist eine Geschäftslogik-Schwachstelle, bei der Software mehrstufige Sitzungen erlaubt, die erfordern, dass Akteure einer bestimmten Reihenfolge folgen, aber diese Reihenfolge nicht ordnungsgemäß validiert. Diese Schwäche tritt auf, wenn Anwendungen nicht durchsetzen, dass Schritte in der erwarteten Reihenfolge ausgeführt werden, dass erforderliche Schritte nicht ausgelassen werden, dass Schritte nicht unterbrochen werden und dass Schritte innerhalb angemessener Zeitlimits ausgeführt werden. Angreifer können dies ausnutzen, indem sie Geschäftslogik manipulieren durch Ausführen von Aktionen außerhalb der Reihenfolge, Überspringen erforderlicher Schritte oder direktes Springen zu finalen Schritten ohne Abschluss der Voraussetzungen.
Risiko
Diese Schwachstelle kann schwerwiegende Sicherheitsauswirkungen haben, indem sie Angreifern erlaubt, kritische Validierungs-, Authentifizierungs- oder Geschäftslogik-Schritte zu umgehen. Ein Angreifer könnte Authentifizierung vollständig überspringen, um auf geschützte Funktionalität zuzugreifen, Zahlungsverifikation in E-Commerce-Transaktionen umgehen, Nutzungsbedingungen-Zustimmungsschritte überspringen, auf administrative Funktionen ohne ordnungsgemäßen Autorisierungs-Workflow zugreifen oder Zustandsmaschinen manipulieren, um privilegierte Zustände zu erreichen. Das Risiko ist besonders hoch in Finanzanwendungen, Authentifizierungsabläufen, mehrstufigen Genehmigungsprozessen und jedem Workflow, bei dem die Schrittreihenfolge Sicherheitsimplikationen hat.
Lösung
Implementieren Sie serverseitige Workflow-Zustandsverwaltung, die den aktuellen Schritt verfolgt und Übergänge validiert. Verwenden Sie sitzungsgebundene Zustandsmaschinen, um gültige Schrittsequenzen durchzusetzen. Verifizieren Sie, dass alle Voraussetzungsschritte abgeschlossen wurden, bevor Sie Zugriff auf nachfolgende Schritte erlauben. Implementieren Sie Schrittabschluss-Tokens, die serverseitig validiert werden. Verlassen Sie sich nicht auf clientseitigen State oder versteckte Formularfelder für Workflow-Durchsetzung. Verwenden Sie datenbankgestützte Workflow-Verfolgung für kritische Prozesse. Implementieren Sie Timeouts für teilweise abgeschlossene Workflows. Erwägen Sie die Verwendung etablierter Workflow-Engines für komplexe mehrstufige Prozesse.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Integrität | Bereich: Integrität Ausführungslogik ändern - Angreifer können das Produkt dazu bringen, kritische Schritte zu überspringen oder sie falsch auszuführen, wodurch beabsichtigte Geschäftslogik umgangen wird. |
| Zugriffskontrolle | Bereich: Zugriffskontrolle Schutzmechanismus umgehen - Sicherheitsprüfungen in übersprungenen Schritten ermöglichen unautorisierten Zugriff auf geschützte Funktionalität. |
| Sonstiges | Bereich: Sonstiges Geschäftliche Auswirkung - Finanzielle Verluste, Datenintegritätsprobleme oder Compliance-Verletzungen durch umgangene Workflows. |
Beispielcode
Anfälliger Code
# Anfällig: FTP-Server ohne Authentifizierungsdurchsetzung
class VulnerableFTPServer:
def __init__(self):
self.authenticated = False
def handle_command(self, command, args):
# Anfällig: Keine Authentifizierungsprüfung für sensible Befehle
if command == 'USER':
self.current_user = args
return "331 Passwort erforderlich"
elif command == 'PASS':
if self.verify_password(self.current_user, args):
self.authenticated = True
return "230 Login erfolgreich"
return "530 Login fehlgeschlagen"
elif command == 'LIST':
# Anfällig: Sollte Authentifizierung erfordern
return self.list_files(args)
elif command == 'RETR':
# Anfällig: Sollte Authentifizierung erfordern
return self.retrieve_file(args)
elif command == 'STOR':
# Anfällig: Sollte Authentifizierung erfordern
return self.store_file(args)
# Angreifer kann direkt LIST, RETR, STOR senden ohne Authentifizierung
// Anfällig: E-Commerce-Checkout ohne Schrittvalidierung
public class VulnerableCheckout {
public void processCheckout(HttpServletRequest request) {
String step = request.getParameter("step");
// Anfällig: Keine Validierung, dass vorherige Schritte abgeschlossen wurden
switch (step) {
case "cart":
displayCart(request);
break;
case "shipping":
processShipping(request);
break;
case "payment":
// Anfällig: Kann direkt hierher springen
processPayment(request);
break;
case "confirm":
// Anfällig: Kann Zahlung vollständig überspringen!
confirmOrder(request);
break;
}
}
// Angreifer kann direkt POST zu step=confirm senden und kostenlose Artikel erhalten
}
// Anfällig: Konto-Einrichtung ohne Schrittdurchsetzung
<?php
class VulnerableAccountSetup {
public function handleSetup($step, $data) {
// Anfällig: Schritte können in beliebiger Reihenfolge ausgeführt werden
switch ($step) {
case 'email':
return $this->setEmail($data['email']);
case 'password':
return $this->setPassword($data['password']);
case 'profile':
return $this->setProfile($data);
case 'activate':
// Anfällig: Kann E-Mail-Verifizierung überspringen
// und Konto direkt aktivieren
return $this->activateAccount();
}
}
}
// Angreifer erstellt Konto und springt zur Aktivierung ohne E-Mail-Verifizierung
?>
// Anfällig: Kreditantrag ohne Workflow-Validierung
class VulnerableLoanApplication {
async processStep(req, res) {
const { step, applicationId, data } = req.body;
// Anfällig: Keine Workflow-Zustandsvalidierung
switch (step) {
case 'apply':
return this.submitApplication(data);
case 'documents':
return this.uploadDocuments(applicationId, data);
case 'verify':
return this.verifyApplication(applicationId);
case 'approve':
// Anfällig: Kann Verifizierung überspringen!
return this.approveApplication(applicationId);
case 'disburse':
// Anfällig: Kann Genehmigung überspringen!
return this.disburseFunds(applicationId);
}
}
}
// Angreifer kann disburse direkt aufrufen ohne Genehmigungsprozess
Korrigierter Code
# Korrigiert: FTP-Server mit ordnungsgemäßer Authentifizierungsdurchsetzung
class FixedFTPServer:
def __init__(self):
self.authenticated = False
self.current_user = None
# Befehle, die Authentifizierung erfordern
AUTHENTICATED_COMMANDS = {'LIST', 'RETR', 'STOR', 'DELE', 'MKD', 'RMD'}
def handle_command(self, command, args):
# Korrigiert: Authentifizierung für geschützte Befehle prüfen
if command in self.AUTHENTICATED_COMMANDS:
if not self.authenticated:
return "530 Bitte zuerst einloggen"
if command == 'USER':
# Authentifizierungszustand zurücksetzen
self.authenticated = False
self.current_user = args
return "331 Passwort erforderlich"
elif command == 'PASS':
if not self.current_user:
return "503 Zuerst mit USER anmelden"
if self.verify_password(self.current_user, args):
self.authenticated = True
return "230 Login erfolgreich"
return "530 Login fehlgeschlagen"
elif command == 'LIST':
return self.list_files(args)
elif command == 'RETR':
return self.retrieve_file(args)
elif command == 'STOR':
return self.store_file(args)
// Korrigiert: E-Commerce-Checkout mit Workflow-Zustandsmaschine
public class FixedCheckout {
enum CheckoutState {
CART, SHIPPING, PAYMENT, CONFIRM, COMPLETE
}
public void processCheckout(HttpServletRequest request, HttpSession session) {
String requestedStep = request.getParameter("step");
// Korrigiert: Aktuellen Workflow-Zustand aus serverseitiger Sitzung holen
CheckoutState currentState = (CheckoutState) session.getAttribute("checkoutState");
if (currentState == null) {
currentState = CheckoutState.CART;
session.setAttribute("checkoutState", currentState);
}
// Korrigiert: Validieren, dass angeforderter Schritt gültige Transition ist
CheckoutState requestedState = CheckoutState.valueOf(requestedStep.toUpperCase());
if (!isValidTransition(currentState, requestedState)) {
throw new InvalidWorkflowException(
"Kann nicht von " + currentState + " zu " + requestedState + " wechseln");
}
switch (requestedState) {
case CART:
displayCart(request);
break;
case SHIPPING:
if (processShipping(request)) {
session.setAttribute("checkoutState", CheckoutState.SHIPPING);
}
break;
case PAYMENT:
if (processPayment(request)) {
session.setAttribute("checkoutState", CheckoutState.PAYMENT);
}
break;
case CONFIRM:
// Korrigiert: Kann nur hierher gelangen nach Zahlung
if (confirmOrder(request)) {
session.setAttribute("checkoutState", CheckoutState.COMPLETE);
}
break;
}
}
private boolean isValidTransition(CheckoutState from, CheckoutState to) {
// Korrigiert: Gültige Zustandsübergänge definieren
switch (from) {
case CART:
return to == CheckoutState.SHIPPING;
case SHIPPING:
return to == CheckoutState.PAYMENT || to == CheckoutState.CART;
case PAYMENT:
return to == CheckoutState.CONFIRM || to == CheckoutState.SHIPPING;
case CONFIRM:
return to == CheckoutState.COMPLETE;
default:
return false;
}
}
}
// Korrigiert: Konto-Einrichtung mit Schrittabschluss-Verfolgung
<?php
class FixedAccountSetup {
private $requiredSteps = ['email', 'verify_email', 'password', 'profile'];
public function handleSetup($userId, $step, $data) {
// Korrigiert: Abgeschlossene Schritte aus Datenbank holen
$completedSteps = $this->getCompletedSteps($userId);
// Korrigiert: Validieren, dass Schritt ausgeführt werden kann
if (!$this->canExecuteStep($step, $completedSteps)) {
throw new WorkflowException("Kann Schritt nicht ausführen: $step");
}
$result = null;
switch ($step) {
case 'email':
$result = $this->setEmail($userId, $data['email']);
if ($result) {
$this->sendVerificationEmail($userId, $data['email']);
}
break;
case 'verify_email':
$result = $this->verifyEmail($userId, $data['token']);
break;
case 'password':
$result = $this->setPassword($userId, $data['password']);
break;
case 'profile':
$result = $this->setProfile($userId, $data);
break;
case 'activate':
// Korrigiert: Alle Schritte müssen abgeschlossen sein
if (count($completedSteps) === count($this->requiredSteps)) {
$result = $this->activateAccount($userId);
} else {
throw new WorkflowException("Alle Schritte vor Aktivierung abschließen");
}
break;
}
if ($result) {
$this->markStepComplete($userId, $step);
}
return $result;
}
private function canExecuteStep($step, $completedSteps) {
// Korrigiert: Voraussetzungen für jeden Schritt definieren
$prerequisites = [
'email' => [],
'verify_email' => ['email'],
'password' => ['verify_email'],
'profile' => ['password'],
'activate' => ['email', 'verify_email', 'password', 'profile']
];
$required = $prerequisites[$step] ?? [];
foreach ($required as $prereq) {
if (!in_array($prereq, $completedSteps)) {
return false;
}
}
return true;
}
}
?>
// Korrigiert: Kreditantrag mit Workflow-Engine
class FixedLoanApplication {
constructor() {
// Korrigiert: Workflow-Zustände und gültige Transitionen definieren
this.workflow = {
states: {
'submitted': { next: ['documents_uploaded'] },
'documents_uploaded': { next: ['verified'] },
'verified': { next: ['approved', 'rejected'] },
'approved': { next: ['disbursed'] },
'rejected': { next: [] },
'disbursed': { next: [] }
}
};
}
async processStep(req, res) {
const { step, applicationId, data } = req.body;
// Korrigiert: Aktuellen Antragszustand aus Datenbank holen
const application = await this.getApplication(applicationId);
if (!application) {
return res.status(404).json({ error: 'Antrag nicht gefunden' });
}
// Korrigiert: Zustandstransition validieren
const targetState = this.getTargetState(step);
if (!this.isValidTransition(application.state, targetState)) {
return res.status(400).json({
error: `Kann nicht von ${application.state} zu ${targetState} wechseln`
});
}
let result;
switch (step) {
case 'apply':
result = await this.submitApplication(data);
break;
case 'documents':
result = await this.uploadDocuments(applicationId, data);
break;
case 'verify':
result = await this.verifyApplication(applicationId);
break;
case 'approve':
// Korrigiert: Erfordert, dass Verifizierung abgeschlossen ist
result = await this.approveApplication(applicationId);
break;
case 'disburse':
// Korrigiert: Erfordert, dass Genehmigung abgeschlossen ist
result = await this.disburseFunds(applicationId);
break;
}
if (result.success) {
// Korrigiert: Zustand in Datenbank aktualisieren
await this.updateApplicationState(applicationId, targetState);
}
return res.json(result);
}
isValidTransition(currentState, targetState) {
const allowed = this.workflow.states[currentState]?.next || [];
return allowed.includes(targetState);
}
}
CVE-Beispiele
- CVE-2010-2620: FTP-Server erlaubte Zugriff auf Dateien ohne ordnungsgemäßen Abschluss des Authentifizierungsschritts.
- CVE-2005-3296: FTP-Server erlaubte Verzeichnisauflistung ohne vorherigen Login-Schritt.
- CVE-2005-3327: Authentifizierungsumgehung möglich durch Überspringen von Startsequenz-Schritten.
- CVE-2004-0829: Server stürzte ab, wenn "find next" ohne vorherige "find first"-Suche ausgeführt wurde.
Verwandte CWEs
- CWE-691: Unzureichendes Kontrollfluss-Management (Eltern)
- CWE-840: Geschäftslogik-Fehler (verwandte Kategorie)
- CWE-696: Falsche Verhaltensreihenfolge (verwandt - fokussiert auf Produktaktionen, nicht Akteursaktionen)
- CWE-362: Nebenläufige Ausführung mit geteilter Ressource und unzureichender Synchronisation (kann beitragen)
Referenzen
- MITRE Corporation. "CWE-841: Improper Enforcement of Behavioral Workflow." https://cwe.mitre.org/data/definitions/841.html
- OWASP. "Business Logic Security Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Business_Logic_Security_Cheat_Sheet.html
- OWASP. "Testing for Business Logic." OWASP Testing Guide.