Weakness reference
CWE-393

Return of Wrong Status Code

This weakness occurs when software returns an HTTP status code or function return value that doesn't match the actual outcome of an operation. For example…

01Summary

This weakness occurs when software returns an HTTP status code or function return value that doesn't match the actual outcome of an operation. For example, returning a 200 (success) when an operation failed, or returning a success code when access was denied. This misleads callers—whether they're browsers, API clients, or other code—into believing an operation succeeded when it didn't, potentially causing security decisions to be made on false information.

02How It Happens

Status codes are a contract between a service and its consumer: they communicate whether a request succeeded, failed, or requires further action. When this contract is broken—either through logic errors, incomplete error handling, or misunderstanding of HTTP semantics—callers cannot reliably determine what actually happened. A common pattern is catching an exception but still returning 200, or checking a permission but returning success anyway. Another is returning a generic 500 error for both "database unavailable" and "user not authenticated," making it impossible for the client to respond appropriately. The root cause is usually insufficient validation of operation outcomes before the status code is chosen.

03Real-World Impact

Incorrect status codes can have serious consequences. An API client that receives 200 for a failed payment might proceed as if the transaction succeeded, leading to inventory loss or financial discrepancies. A web application that returns 200 for a failed login attempt might cause client-side code to grant access or skip re-authentication. Security tools and monitoring systems that rely on status codes to detect breaches may miss actual failures. In OAuth or token-based systems, returning 200 when a token is invalid can allow expired credentials to be reused. The impact ranges from data inconsistency to authentication bypass, depending on what decision the caller makes based on the wrong code.

04Vulnerable & Fixed Patterns

Vulnerable pattern
def process_payment(user_id, amount):
    try:
        account = get_account(user_id)
        if account.balance < amount:
            return {"status": 200, "message": "Insufficient funds"}
        account.balance -= amount
        save_account(account)
        return {"status": 200, "message": "Payment processed"}
    except Exception as e:
        return {"status": 200, "message": "Error occurred"}

Why it's vulnerable:
All outcomes—success, insufficient funds, and exceptions—return status 200, making it impossible for the caller to distinguish between a successful payment and a failed one.

Fixed pattern
def process_payment(user_id, amount):
    try:
        account = get_account(user_id)
        if account.balance < amount:
            return {"status": 402, "message": "Insufficient funds"}
        account.balance -= amount
        save_account(account)
        return {"status": 200, "message": "Payment processed"}
    except Exception as e:
        return {"status": 500, "message": "Server error"}
Vulnerable pattern
function verify_user_access($user_id, $resource_id) {
    $user = get_user($user_id);
    $resource = get_resource($resource_id);
    
    if (!$user || !$resource) {
        http_response_code(200);
        return json_encode(["access" => "denied"]);
    }
    
    if ($user->role !== "admin") {
        http_response_code(200);
        return json_encode(["access" => "denied"]);
    }
    
    http_response_code(200);
    return json_encode(["access" => "granted"]);
}

Why it's vulnerable:
Both denied and granted access return HTTP 200, so a client checking only the status code will treat denial as success.

Fixed pattern
function verify_user_access($user_id, $resource_id) {
    $user = get_user($user_id);
    $resource = get_resource($resource_id);
    
    if (!$user || !$resource) {
        http_response_code(404);
        return json_encode(["error" => "Not found"]);
    }
    
    if ($user->role !== "admin") {
        http_response_code(403);
        return json_encode(["error" => "Forbidden"]);
    }
    
    http_response_code(200);
    return json_encode(["access" => "granted"]);
}

05Prevention Checklist

Map every possible outcome of an operation (success, validation failure, permission denial, server error, timeout) to the correct HTTP status code or return value before writing the handler.
Use standard status codes correctly: 200 for success, 400 for client error, 401 for authentication failure, 403 for authorization failure, 404 for not found, 500 for server error.
Test error paths explicitly—don't assume exceptions are handled correctly; verify the status code returned in each failure scenario.
Document the status codes your API returns for each endpoint, including all error cases, and keep documentation in sync with code.
Use a linter or code review checklist to catch cases where exceptions are caught but status codes are not updated accordingly.
For internal functions, use explicit return codes or exceptions rather than relying on implicit success; make failure states obvious.

06Signs You May Already Be Affected

Review your application logs for patterns where operations that should have failed (e.g., invalid input, permission denied, database errors) are recorded as returning 200 or success codes. Check your API documentation against actual behavior: if callers report that they received success responses for operations that didn't actually complete, or if monitoring tools are missing failures, incorrect status codes may be the cause. Audit recent security incidents or data inconsistencies to see whether callers made decisions based on status codes that didn't reflect reality.