Fail-open vulnerabilities occur when applications default to allowing access or trusting input when security checks fail or logic errors occur.
The opposite of proper security is "fail-open" — when something goes wrong, the system defaults to allowing everything instead of denying everything. This is the most dangerous category of logic errors because it actively weakens security.
Real-World Attack Scenarios
Scenario 1: Failing Open on Authorization Check (CWE-636)
An application checks if user is admin:
defdelete_user(user_id):# Check if current user is admintry: is_admin =check_admin_status()except:# FAIL OPEN - Assume admin on error!pass# No check if is_admin is True!# If exception, is_admin is undefined!# Proceed with deletion db.delete_user(user_id)return{"status":"User deleted"}
The vulnerability:
If check_admin_status() throws exception:
Exception caught but not handled properly
is_admin undefined
Code proceeds anyway
User deleted even though authorization failed
The attack:
Attacker sends request while admin check service is down:
Admin check service fails with connection error:
Exception thrown
Caught and ignored (pass statement)
Code proceeds anyway
User 42 deleted by non-admin attacker
Result:
Complete authorization bypass
Unauthorized admin actions
Data deletion
Service unavailable = security disabled
The fix:
Fail securely (default deny):
Finding it: Look for try/except blocks that catch security checks. Check if exception handling leads to access grant rather than denial.
Scenario 2: Missing Break Statement in Switch (CWE-484)
User role authorization check missing break:
The vulnerability:
Missing break causes fall-through:
role = "user" sets accessLevel = 1
Falls through to admin case
Overrides with accessLevel = 10
User becomes admin!
The attack:
Attacker registers as regular user, then somehow triggers the "user" case followed by admin case:
Result:
Privilege escalation
Regular user becomes admin
Unauthorized access
The fix:
Always include break statements:
Finding it: Search for switch statements without break. Use compiler warnings. Test for fall-through behavior.
Scenario 3: Missing Default Case (CWE-478)
Payment status check missing default case:
The vulnerability:
No default case for unexpected values:
status = "approved" → Delivers (correct)
status = "declined" → Declines (correct)
status = "pending" → Falls through, does nothing
status = "refund" → Falls through, does nothing
status = "invalid" → Falls through, does nothing
Attacker sends status = "pending":
No delivery
No payment charge
Free product
The attack:
Attacker modifies API call:
Result:
Free products
Revenue loss
Fraud
The fix:
Always have default case:
Or use explicit state machine:
Finding it: Look for if/elif chains without else. Look for switch statements without default. Check for unhandled enum values.
Scenario 4: NULL Pointer Dereference (CWE-476)
User authorization check returns NULL:
The vulnerability:
If getCurrentUserId() returns invalid ID:
findUserById() returns null
user.isAdmin() throws NullPointerException
Exception caught and ignored
Authorization check fails silently
Code continues anyway
User deletion proceeds without authorization check
The attack:
Attacker sends request with invalid user ID:
findUserById("invalid") returns null
isAdmin() throws NullPointerException
Exception silently caught
deleteUser() proceeds
User deleted without proper authorization
Result:
Authorization bypass
Unauthorized deletion
Data destruction
The fix:
Always check for null:
Finding it: Search for method calls without null checks. Use static analysis tools (FindBugs, Checkstyle). Enable compiler warnings for null dereferences.
Scenario 5: Divide By Zero (CWE-369)
Price calculation with user-provided denominator:
Normal case:
The attack:
Attacker provides 0 discount:
Actually, divide by zero needs different scenario. Better example:
The attack:
Attacker provides 0 items:
Result:
Divide by zero exception
Application crash
Denial of service
The fix:
Validate input:
Finding it: Look for division operations. Check if denominator is validated. Test with zero values.
Scenario 6: Inverted Authorization Logic
Authorization check logic accidentally inverted:
The vulnerability:
Logic is backwards:
If user_id == current_user.id → Deny (your own profile)
If user_id != current_user.id → Allow (someone else's profile)
The attack:
Attacker gets other user's ID (123) and modifies their profile:
Authorization check:
user_id = 123 (victim)
current_user.id = 456 (attacker)
123 != 456 → Allow!
Attacker modifies victim's profile
Result:
Account takeover
Profile defacement
Email change
Password reset
The fix:
Correct the logic:
Finding it: Review authorization logic carefully. Test with unauthorized users. Look for inverted conditions (not equals instead of equals).
Scenario 7: Default Allow in Access Control
Access control matrix with missing entries defaults to allow:
The vulnerability:
Actually, this one defaults to deny. Better example:
The attack:
Attacker sends empty or null role:
Result:
Complete authorization bypass
Unauthorized access
Admin actions as non-admin
The fix:
Always validate and fail securely:
Finding it: Review access control logic. Check for default allow. Look for missing validation. Test with null/empty values.
curl -X POST http://target.com/admin/delete-user \
-d '{"user_id": 42}'
def delete_user(user_id):
try:
is_admin = check_admin_status()
except Exception as e:
logger.error(f"Admin check failed: {e}")
# FAIL SECURE - Deny access
return {"error": "Authorization check failed"}, 403
if not is_admin:
return {"error": "Unauthorized"}, 403
# Only proceed if check succeeded AND user is admin
db.delete_user(user_id)
return {"status": "User deleted"}
public void checkAccess(String role) {
switch(role) {
case "user":
// User permissions
accessLevel = 1;
// MISSING break!
case "admin":
// Admin permissions
accessLevel = 10;
break;
case "guest":
accessLevel = 0;
break;
}
return accessLevel;
}
checkAccess("user") // Sets accessLevel = 1, then falls through
// accessLevel = 10 (admin!)
public void checkAccess(String role) {
switch(role) {
case "user":
accessLevel = 1;
break; // Prevent fall-through
case "admin":
accessLevel = 10;
break;
case "guest":
accessLevel = 0;
break;
default:
accessLevel = 0; // Default deny
break;
}
return accessLevel;
}
def process_payment(status):
if status == "approved":
deliver_product()
elif status == "declined":
send_decline_message()
# MISSING else/default!
# If status is anything else, what happens?
# Falls through without doing anything
curl -X POST http://target.com/process-payment \
-d '{"status": "pending", "product": "iPhone"}'
# Payment process doesn't handle "pending"
# Falls through default case (missing)
# Product delivered without payment!
def process_payment(status):
if status == "approved":
charge_card()
deliver_product()
elif status == "declined":
send_decline_message()
else:
# Default: deny everything
logger.warning(f"Unknown payment status: {status}")
send_error_message()
return False
return True
VALID_STATUSES = {"approved", "declined", "pending"}
def process_payment(status):
if status not in VALID_STATUSES:
raise InvalidStatusError(f"Invalid status: {status}")
if status == "approved":
deliver_product()
elif status == "declined":
send_decline_message()
# All valid cases handled
public boolean isAdmin(int userId) {
User user = findUserById(userId);
// VULNERABLE: No null check!
// If user is null, next line crashes
return user.isAdmin(); // NullPointerException if user is null
}
public void deleteUser(int targetId) {
try {
if (isAdmin(getCurrentUserId())) {
// Delete the user
db.deleteUser(targetId);
}
} catch (NullPointerException e) {
// FAIL OPEN - Exception caught, ignored
// No error handling
}
return;
}
curl -X POST http://target.com/delete-user \
-H "User-ID: invalid"
public boolean isAdmin(int userId) {
User user = findUserById(userId);
// Check for null
if (user == null) {
throw new UserNotFoundException("User not found");
}
return user.isAdmin();
}
public void deleteUser(int targetId) {
try {
if (isAdmin(getCurrentUserId())) {
db.deleteUser(targetId);
} else {
throw new UnauthorizedException("Not authorized");
}
} catch (UserNotFoundException | UnauthorizedException e) {
// Handle specific exceptions
return {"error": "Authorization failed"}, 403;
}
}
def calculate_price_per_item(total_price, num_items):
# Validate input
if num_items <= 0:
raise ValueError("num_items must be > 0")
if total_price < 0:
raise ValueError("total_price must be >= 0")
price_per_item = total_price / num_items
return price_per_item
def edit_profile(user_id, user_data):
# Get current user
current_user = get_current_user()
# VULNERABLE: Logic inverted!
if user_id == current_user.id:
# Only owner should be able to edit
# But if NOT owner, proceed!
return {"error": "Cannot edit your own profile"}
else:
# If editing someone else's profile, allow!
update_user(user_id, user_data)
return {"status": "Profile updated"}
ACCESS_MATRIX = {
"admin": {
"view_users": True,
"edit_users": True,
"delete_users": True,
},
"user": {
"view_users": False,
"edit_users": False,
},
# "guest" role completely missing!
}
def can_access(role, action):
# If role not in matrix, or action not in role:
# Default returns None, which is falsy
# So access denied? Let's check...
if role not in ACCESS_MATRIX:
return False # Actually denies
# But what if action missing?
return ACCESS_MATRIX[role].get(action) # Returns None (falsy)
def can_access(role, action):
# Missing check for empty/null role
if not role:
# FAIL OPEN - Empty role defaults to admin?
return True # BUG!
return ACCESS_MATRIX[role].get(action, False)
curl -X POST http://target.com/admin/action \
-d '{"role": "", "action": "delete"}'
# can_access("", "delete")
# Empty role defaults to True
# Authorization check passes!
# Attacker gains admin access
def can_access(role, action):
# Validate role
if not role or role not in ACCESS_MATRIX:
return False # Default deny
# Validate action exists in matrix
if action not in ACCESS_MATRIX[role]:
return False # Default deny
# Check permission
return ACCESS_MATRIX[role][action]