Fail Secure
Status: Complete
Category: Security
Default enforcement: Soft
Author: PushBackLog team
Tags
- Topic: security, reliability
- Skillset: backend, frontend
- Technology: generic
- Stage: execution, review
Summary
Fail secure (also called fail closed or fail safe) means that when a system encounters an error in a security-relevant operation, it defaults to the more restrictive, safer state rather than the more permissive one. An authorisation check that throws an unexpected exception should deny access, not grant it. A feature flag service that is unavailable should disable the feature, not enable it. A payment processing error should not complete the transaction. When a system fails, the failure should not create an opening.
Rationale
Failures in security code are not neutral
In non-security code, an unexpected error is typically handled with logging and a generic error response — the consequence is a failed operation. In security code, an unhandled error can have a more serious consequence: if the code path that was supposed to enforce a restriction failed silently, the restriction may have been skipped entirely.
Consider an authorisation middleware that calls an external permission service. If the call fails and the middleware catches the exception and calls next() to continue processing the request, the access check has been bypassed for all requests during the outage. This is fail-open: the failure opens the gate. The correct behaviour is fail-closed: deny the request and return a safe error.
The default is commonly wrong
Most error handling defaults are fail-open because developers optimise for availability and user experience during development. An error in a discount code validation might be caught and treated as “code not found” — granting the original price rather than the discounted one. An error in a subscription tier check might be caught and treated as “highest tier” rather than “deny access.”
Reviewing code for fail-open conditions in security-critical paths is a specific security review activity, not a consequence of general error handling quality.
Guidance
Identifying fail-open conditions
Look for security-relevant operations in try/catch blocks where the catch handler continues the happy path:
// WRONG — fail open in authorisation check
async function checkPermission(userId: string, resource: string): Promise<boolean> {
try {
return await permissionService.check(userId, resource);
} catch (e) {
logger.warn({ e }, 'Permission check failed');
return true; // DANGEROUS: grants access on error
}
}
// CORRECT — fail closed
async function checkPermission(userId: string, resource: string): Promise<boolean> {
try {
return await permissionService.check(userId, resource);
} catch (e) {
logger.error({ e, userId, resource }, 'Permission check failed — denying');
return false; // Safe default: deny access when uncertain
}
}
Common fail-open patterns to eliminate
| Scenario | Fail-open (dangerous) | Fail-closed (correct) |
|---|---|---|
| Authorisation service unavailable | Grant access | Deny access with 503 |
| Feature flag service unavailable | Enable feature | Disable feature (safe default) |
| Rate limiter throws exception | Allow request | Deny request (conservative) |
| Payment amount validation fails | Process payment | Reject transaction |
| Signature verification throws | Accept payload | Reject payload |
| Session lookup fails | Create new session with existing claims | Require re-authentication |
| Multi-factor auth service unavailable | Skip MFA check | Block login |
Principle applied broadly
Fail secure extends beyond authorisation:
// Input validation: fail closed
function parseCurrency(input: unknown): 'GBP' | 'USD' | 'EUR' {
const allowed = ['GBP', 'USD', 'EUR'] as const;
if (!allowed.includes(input as any)) {
throw new ValidationError('Invalid currency'); // Reject, never default to a currency
}
return input as 'GBP' | 'USD' | 'EUR';
}
// Webhook signature: fail closed
async function handleWebhook(req: Request): Promise<void> {
const isValid = validateSignature(req.body, req.headers['x-signature']);
if (!isValid) {
// Never process on validation failure — even if it means missing an event
logger.warn('Invalid webhook signature — rejecting');
throw new UnauthorizedError();
}
await processWebhookEvent(req.body);
}
Secure defaults in configuration
Fail secure also applies to system configuration:
- New IAM roles and policies should start with zero permissions (fail closed); add permissions explicitly
- New API endpoints should require authentication unless explicitly marked public
- New feature flags should default to disabled (not enabled)
- New service-to-service connections should be denied by network policy until explicitly allowed
Review checklist
- Authorisation failures default to deny, not allow
- Exception handlers in security-critical paths do not grant access or continue privileged operations
- Feature flags default to the safe/disabled state when the flag service is unavailable
- New IAM policies, network policies, and API routes start from a closed/deny default
- Signature and token validation failures always reject the request
- Security-relevant error conditions produce alerts, not silent degradation