Improper Enforcement of Behavioral Workflow
Description
Improper Enforcement of Behavioral Workflow is a business logic vulnerability where software permits multi-step sessions that require actors to follow a specific sequence but fails to properly validate this ordering. This weakness occurs when applications don't enforce that steps are performed in the expected order, that required steps aren't omitted, that steps aren't interrupted, and that steps are executed within appropriate time limits. Attackers can exploit this by manipulating business logic through performing actions out of sequence, skipping required steps, or jumping directly to final steps without completing prerequisites.
Risk
This vulnerability can have severe security implications by allowing attackers to bypass critical validation, authentication, or business logic steps. An attacker might skip authentication entirely to access protected functionality, bypass payment verification in e-commerce transactions, skip terms of service agreement steps, access administrative functions without proper authorization workflow, or manipulate state machines to reach privileged states. The risk is particularly high in financial applications, authentication flows, multi-step approval processes, and any workflow where step ordering has security implications.
Solution
Implement server-side workflow state management that tracks the current step and validates transitions. Use session-bound state machines to enforce valid step sequences. Verify that all prerequisite steps have been completed before allowing access to subsequent steps. Implement step completion tokens that are validated server-side. Don't rely on client-side state or hidden form fields for workflow enforcement. Use database-backed workflow tracking for critical processes. Implement timeouts for partially completed workflows. Consider using established workflow engines for complex multi-step processes.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Alter Execution Logic - Attackers can cause the product to skip critical steps or perform them incorrectly, bypassing intended business logic. |
| Access Control | Scope: Access Control Bypass Protection Mechanism - Security checks in skipped steps allow unauthorized access to protected functionality. |
| Other | Scope: Other Business Impact - Financial losses, data integrity issues, or compliance violations from bypassed workflows. |
Example Code
Vulnerable Code
# Vulnerable: FTP server without authentication enforcement
class VulnerableFTPServer:
def __init__(self):
self.authenticated = False
def handle_command(self, command, args):
# Vulnerable: No authentication check for sensitive commands
if command == 'USER':
self.current_user = args
return "331 Password required"
elif command == 'PASS':
if self.verify_password(self.current_user, args):
self.authenticated = True
return "230 Login successful"
return "530 Login failed"
elif command == 'LIST':
# Vulnerable: Should require authentication
return self.list_files(args)
elif command == 'RETR':
# Vulnerable: Should require authentication
return self.retrieve_file(args)
elif command == 'STOR':
# Vulnerable: Should require authentication
return self.store_file(args)
# Attacker can directly send LIST, RETR, STOR without authenticating
// Vulnerable: E-commerce checkout without step validation
public class VulnerableCheckout {
public void processCheckout(HttpServletRequest request) {
String step = request.getParameter("step");
// Vulnerable: No validation that previous steps completed
switch (step) {
case "cart":
displayCart(request);
break;
case "shipping":
processShipping(request);
break;
case "payment":
// Vulnerable: Can jump directly here
processPayment(request);
break;
case "confirm":
// Vulnerable: Can skip payment entirely!
confirmOrder(request);
break;
}
}
// Attacker can POST directly to step=confirm and get free items
}
// Vulnerable: Account setup without step enforcement
<?php
class VulnerableAccountSetup {
public function handleSetup($step, $data) {
// Vulnerable: Steps can be executed in any order
switch ($step) {
case 'email':
return $this->setEmail($data['email']);
case 'password':
return $this->setPassword($data['password']);
case 'profile':
return $this->setProfile($data);
case 'activate':
// Vulnerable: Can skip email verification
// and directly activate account
return $this->activateAccount();
}
}
}
// Attacker creates account and jumps to activation without email verification
?>
// Vulnerable: Loan application without workflow validation
class VulnerableLoanApplication {
async processStep(req, res) {
const { step, applicationId, data } = req.body;
// Vulnerable: No workflow state validation
switch (step) {
case 'apply':
return this.submitApplication(data);
case 'documents':
return this.uploadDocuments(applicationId, data);
case 'verify':
return this.verifyApplication(applicationId);
case 'approve':
// Vulnerable: Can skip verification!
return this.approveApplication(applicationId);
case 'disburse':
// Vulnerable: Can skip approval!
return this.disburseFunds(applicationId);
}
}
}
// Attacker can call disburse directly without going through approval
Fixed Code
# Fixed: FTP server with proper authentication enforcement
class FixedFTPServer:
def __init__(self):
self.authenticated = False
self.current_user = None
# Commands that require authentication
AUTHENTICATED_COMMANDS = {'LIST', 'RETR', 'STOR', 'DELE', 'MKD', 'RMD'}
def handle_command(self, command, args):
# Fixed: Check authentication for protected commands
if command in self.AUTHENTICATED_COMMANDS:
if not self.authenticated:
return "530 Please login first"
if command == 'USER':
# Reset authentication state
self.authenticated = False
self.current_user = args
return "331 Password required"
elif command == 'PASS':
if not self.current_user:
return "503 Login with USER first"
if self.verify_password(self.current_user, args):
self.authenticated = True
return "230 Login successful"
return "530 Login failed"
elif command == 'LIST':
return self.list_files(args)
elif command == 'RETR':
return self.retrieve_file(args)
elif command == 'STOR':
return self.store_file(args)
// Fixed: E-commerce checkout with workflow state machine
public class FixedCheckout {
enum CheckoutState {
CART, SHIPPING, PAYMENT, CONFIRM, COMPLETE
}
public void processCheckout(HttpServletRequest request, HttpSession session) {
String requestedStep = request.getParameter("step");
// Fixed: Get current workflow state from server-side session
CheckoutState currentState = (CheckoutState) session.getAttribute("checkoutState");
if (currentState == null) {
currentState = CheckoutState.CART;
session.setAttribute("checkoutState", currentState);
}
// Fixed: Validate requested step is valid transition
CheckoutState requestedState = CheckoutState.valueOf(requestedStep.toUpperCase());
if (!isValidTransition(currentState, requestedState)) {
throw new InvalidWorkflowException(
"Cannot go from " + currentState + " to " + requestedState);
}
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:
// Fixed: Can only reach here after payment
if (confirmOrder(request)) {
session.setAttribute("checkoutState", CheckoutState.COMPLETE);
}
break;
}
}
private boolean isValidTransition(CheckoutState from, CheckoutState to) {
// Fixed: Define valid state transitions
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;
}
}
}
// Fixed: Account setup with step completion tracking
<?php
class FixedAccountSetup {
private $requiredSteps = ['email', 'verify_email', 'password', 'profile'];
public function handleSetup($userId, $step, $data) {
// Fixed: Get completed steps from database
$completedSteps = $this->getCompletedSteps($userId);
// Fixed: Validate step can be executed
if (!$this->canExecuteStep($step, $completedSteps)) {
throw new WorkflowException("Cannot execute step: $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':
// Fixed: All steps must be completed
if (count($completedSteps) === count($this->requiredSteps)) {
$result = $this->activateAccount($userId);
} else {
throw new WorkflowException("Complete all steps before activation");
}
break;
}
if ($result) {
$this->markStepComplete($userId, $step);
}
return $result;
}
private function canExecuteStep($step, $completedSteps) {
// Fixed: Define prerequisites for each step
$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;
}
}
?>
// Fixed: Loan application with workflow engine
class FixedLoanApplication {
constructor() {
// Fixed: Define workflow states and valid transitions
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;
// Fixed: Get current application state from database
const application = await this.getApplication(applicationId);
if (!application) {
return res.status(404).json({ error: 'Application not found' });
}
// Fixed: Validate state transition
const targetState = this.getTargetState(step);
if (!this.isValidTransition(application.state, targetState)) {
return res.status(400).json({
error: `Cannot transition from ${application.state} to ${targetState}`
});
}
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':
// Fixed: Requires verification to be complete
result = await this.approveApplication(applicationId);
break;
case 'disburse':
// Fixed: Requires approval to be complete
result = await this.disburseFunds(applicationId);
break;
}
if (result.success) {
// Fixed: Update state in database
await this.updateApplicationState(applicationId, targetState);
}
return res.json(result);
}
isValidTransition(currentState, targetState) {
const allowed = this.workflow.states[currentState]?.next || [];
return allowed.includes(targetState);
}
}
CVE Examples
- CVE-2010-2620: FTP server allowed access to files without proper authentication step completion.
- CVE-2005-3296: FTP server allowed listing directories without requiring login step first.
- CVE-2005-3327: Authentication bypass possible by skipping startup sequence steps.
- CVE-2004-0829: Server crashed when "find next" was issued without prior "find first" search.
Related CWEs
- CWE-691: Insufficient Control Flow Management (parent)
- CWE-840: Business Logic Errors (related category)
- CWE-696: Incorrect Behavior Order (related - focuses on product actions, not actor actions)
- CWE-362: Concurrent Execution Using Shared Resource with Improper Synchronization (can contribute)
References
- 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.