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

ImpactDetails
IntegrityScope: Integrity

Alter Execution Logic - Attackers can cause the product to skip critical steps or perform them incorrectly, bypassing intended business logic.
Access ControlScope: Access Control

Bypass Protection Mechanism - Security checks in skipped steps allow unauthorized access to protected functionality.
OtherScope: 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.

  • 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

  1. MITRE Corporation. "CWE-841: Improper Enforcement of Behavioral Workflow." https://cwe.mitre.org/data/definitions/841.html
  2. OWASP. "Business Logic Security Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Business_Logic_Security_Cheat_Sheet.html
  3. OWASP. "Testing for Business Logic." OWASP Testing Guide.