Reliance on a Single Factor in a Security Decision
Description
Reliance on a Single Factor in a Security Decision occurs when an application uses only one piece of information to make security-critical decisions. This includes using only a password without MFA, trusting only IP address for access control, relying solely on client-provided data, or using a single check for authorization. If that single factor is compromised, the entire security control fails.
Risk
Credential theft leads to complete account compromise. IP spoofing bypasses network-based access controls. Single points of failure in security architecture. Social engineering attacks succeed with one piece of information. Replay attacks succeed when tokens aren't combined with other factors. Automated attacks are easier without multiple challenges.
Solution
Implement multi-factor authentication. Use defense in depth with multiple security layers. Combine something you know, have, and are. Verify multiple independent attributes. Implement risk-based authentication. Use behavioral analysis alongside credentials. Don't rely solely on any single identifier.
Common Consequences
| Impact | Details |
|---|---|
| Authentication | Scope: Account Takeover Single factor compromise grants full access. |
| Authorization | Scope: Access Control Bypass Single check easily circumvented. |
| Non-Repudiation | Scope: Impersonation Cannot prove user identity with single factor. |
Example Code + Solution Code
Vulnerable Code
// VULNERABLE: Password-only authentication
public class VulnerableSingleFactorAuth {
public AuthResult authenticate(String username, String password) {
User user = userRepository.findByUsername(username);
// VULNERABLE: Only password check
if (user != null && passwordEncoder.matches(password, user.getPasswordHash())) {
return AuthResult.success(user);
}
return AuthResult.failure();
}
}
// VULNERABLE: IP-only access control
public class VulnerableIPBasedAuth {
private Set<String> allowedIPs = Set.of("10.0.0.1", "10.0.0.2");
public boolean isAuthorized(HttpServletRequest request) {
// VULNERABLE: IP address can be spoofed
String clientIP = request.getRemoteAddr();
return allowedIPs.contains(clientIP);
}
}
// VULNERABLE: Single token for all access
public class VulnerableSingleTokenAuth {
public boolean validateAccess(String token) {
// VULNERABLE: Token alone grants all access
// If token is stolen, attacker has full access
return tokenService.isValid(token);
}
public void performSensitiveAction(String token) {
// VULNERABLE: No additional verification for sensitive actions
if (validateAccess(token)) {
executeSensitiveOperation();
}
}
}
// VULNERABLE: Referer-only CSRF protection
public class VulnerableRefererCheck {
public boolean isValidRequest(HttpServletRequest request) {
// VULNERABLE: Referer can be manipulated/removed
String referer = request.getHeader("Referer");
return referer != null && referer.startsWith("https://myapp.com");
}
}
# VULNERABLE: Single factor authentication
class VulnerableAuth:
def authenticate(self, username, password):
user = get_user(username)
# VULNERABLE: Password only
if user and check_password(password, user.password_hash):
return create_session(user)
return None
# VULNERABLE: API key only
def authenticate_api(self, api_key):
# Single key grants full API access
if api_key in valid_api_keys:
return True
return False
# VULNERABLE: Cookie-only session validation
class VulnerableSessionAuth:
def validate_session(self, session_id):
# VULNERABLE: Only checks session cookie
# If cookie is stolen (XSS), attacker has full access
session = get_session(session_id)
return session is not None
def transfer_funds(self, session_id, amount, to_account):
# VULNERABLE: No re-authentication for sensitive action
if self.validate_session(session_id):
execute_transfer(amount, to_account)
# VULNERABLE: Secret question only for password reset
class VulnerablePasswordReset:
def verify_identity(self, username, answer):
user = get_user(username)
# VULNERABLE: Single question, often guessable
if user.security_answer.lower() == answer.lower():
return True
return False
def reset_password(self, username, answer, new_password):
# VULNERABLE: Single factor for critical action
if self.verify_identity(username, answer):
set_password(username, new_password)
// VULNERABLE: Single factor auth in Node.js
class VulnerableAuth {
async authenticate(username, password) {
const user = await db.findUser(username);
// VULNERABLE: Password only
if (user && await bcrypt.compare(password, user.passwordHash)) {
return this.createToken(user);
}
return null;
}
// VULNERABLE: Bearer token only
validateRequest(req) {
const token = req.headers.authorization?.split(' ')[1];
// VULNERABLE: Token alone grants access
// No device fingerprinting, no location check
return this.verifyToken(token);
}
}
// VULNERABLE: Origin-only CORS protection
app.use((req, res, next) => {
const origin = req.headers.origin;
// VULNERABLE: Origin can be spoofed in some scenarios
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
next();
});
// VULNERABLE: User-Agent for bot detection
function isBot(req) {
// VULNERABLE: User-Agent easily spoofed
const ua = req.headers['user-agent'];
return botPatterns.some(pattern => pattern.test(ua));
}
// VULNERABLE: Email verification only for account recovery
async function recoverAccount(email) {
const user = await db.findByEmail(email);
if (user) {
// VULNERABLE: Email access is single factor
// If email is compromised, account is compromised
const token = generateResetToken();
await sendResetEmail(email, token);
}
}
<?php
// VULNERABLE: Password-only authentication
class VulnerableAuth {
public function login($username, $password) {
$user = $this->getUser($username);
// VULNERABLE: Single factor
if ($user && password_verify($password, $user['password_hash'])) {
$_SESSION['user_id'] = $user['id'];
return true;
}
return false;
}
// VULNERABLE: Session-only for admin actions
public function deleteUser($targetUserId) {
// VULNERABLE: No re-authentication for destructive action
if (isset($_SESSION['user_id'])) {
$this->db->delete('users', $targetUserId);
return true;
}
return false;
}
}
// VULNERABLE: API key only
function authenticateAPI() {
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
// VULNERABLE: Key alone grants full access
return in_array($apiKey, getValidApiKeys());
}
// VULNERABLE: CAPTCHA only for registration
function registerUser($data, $captchaResponse) {
// VULNERABLE: CAPTCHA is only bot prevention
// No email verification, no phone verification
if (verifyCaptcha($captchaResponse)) {
createUser($data);
return true;
}
return false;
}
?>
Fixed Code
// SAFE: Multi-factor authentication
public class MultiFactorAuth {
private final PasswordEncoder passwordEncoder;
private final TotpService totpService;
private final RiskAnalysisService riskService;
public AuthResult authenticate(AuthRequest request) {
User user = userRepository.findByUsername(request.getUsername());
if (user == null) {
return AuthResult.failure("Invalid credentials");
}
// Factor 1: Password (something you know)
if (!passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) {
return AuthResult.failure("Invalid credentials");
}
// Factor 2: TOTP (something you have)
if (user.hasMfaEnabled()) {
if (!totpService.verify(user.getTotpSecret(), request.getTotpCode())) {
return AuthResult.failure("Invalid MFA code");
}
}
// Factor 3: Risk-based checks (behavioral)
RiskAssessment risk = riskService.assess(request, user);
if (risk.isHigh()) {
// Require additional verification
return AuthResult.requireAdditionalVerification(risk.getReason());
}
return AuthResult.success(user);
}
// SAFE: Re-authentication for sensitive actions
public void performSensitiveAction(User user, String password, String totpCode) {
// Re-verify credentials before sensitive action
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
throw new AuthenticationException("Invalid password");
}
if (user.hasMfaEnabled()) {
if (!totpService.verify(user.getTotpSecret(), totpCode)) {
throw new AuthenticationException("Invalid MFA code");
}
}
executeSensitiveOperation();
}
}
// SAFE: Multiple factors for access control
public class MultiFactorAccessControl {
public boolean isAuthorized(HttpServletRequest request, User user) {
// Factor 1: Valid session
if (!sessionService.isValidSession(request)) {
return false;
}
// Factor 2: IP in allowed range (for admin)
if (user.isAdmin()) {
String ip = getClientIP(request);
if (!allowedAdminIPs.contains(ip)) {
return false;
}
}
// Factor 3: Device fingerprint matches known device
String fingerprint = request.getHeader("X-Device-Fingerprint");
if (!user.getKnownDevices().contains(fingerprint)) {
// Flag for additional verification
notifySecurityTeam(user, "Unknown device access");
}
// Factor 4: No anomalous behavior
if (riskService.detectAnomaly(user, request)) {
return false;
}
return true;
}
}
# SAFE: Multi-factor authentication in Python
import pyotp
from flask import request
class MultiFactorAuth:
def __init__(self, risk_service, device_service):
self.risk_service = risk_service
self.device_service = device_service
def authenticate(self, username, password, totp_code=None, device_id=None):
user = get_user(username)
if not user:
return None
# Factor 1: Password
if not check_password(password, user.password_hash):
self.log_failed_attempt(username, 'password')
return None
# Factor 2: TOTP (if enabled)
if user.mfa_enabled:
if not totp_code:
return {'status': 'mfa_required'}
totp = pyotp.TOTP(user.totp_secret)
if not totp.verify(totp_code):
self.log_failed_attempt(username, 'totp')
return None
# Factor 3: Device verification
if device_id:
if not self.device_service.is_known_device(user.id, device_id):
# New device - send verification email
self.send_device_verification(user, device_id)
return {'status': 'device_verification_required'}
# Factor 4: Risk assessment
risk = self.risk_service.assess(user, request)
if risk.score > 0.8:
return {'status': 'additional_verification_required', 'reason': risk.factors}
return {'status': 'success', 'user': user}
def transfer_funds(self, session, amount, to_account, password, totp_code):
user = session.user
# Re-authenticate for sensitive action
if not check_password(password, user.password_hash):
raise AuthenticationError("Invalid password")
if user.mfa_enabled:
totp = pyotp.TOTP(user.totp_secret)
if not totp.verify(totp_code):
raise AuthenticationError("Invalid MFA code")
# Additional check: transaction limits
if amount > user.transaction_limit:
raise SecurityError("Exceeds transaction limit")
execute_transfer(user, amount, to_account)
# SAFE: Multi-layer API authentication
class SecureAPIAuth:
def authenticate_request(self, request):
# Factor 1: API key
api_key = request.headers.get('X-API-Key')
if not api_key or api_key not in valid_api_keys:
return None
# Factor 2: Request signature (HMAC)
signature = request.headers.get('X-Signature')
expected_sig = self.compute_signature(request, api_key)
if not hmac.compare_digest(signature, expected_sig):
return None
# Factor 3: Timestamp (prevent replay)
timestamp = request.headers.get('X-Timestamp')
if not self.is_recent_timestamp(timestamp):
return None
# Factor 4: IP whitelist (if configured)
api_config = get_api_config(api_key)
if api_config.ip_whitelist:
if request.remote_addr not in api_config.ip_whitelist:
return None
return api_config.client_id
// SAFE: Multi-factor authentication in Node.js
const speakeasy = require('speakeasy');
class MultiFactorAuth {
async authenticate(credentials) {
const { username, password, totpCode, deviceId } = credentials;
const user = await db.findUser(username);
if (!user) {
return { success: false };
}
// Factor 1: Password
if (!await bcrypt.compare(password, user.passwordHash)) {
await this.logFailedAttempt(username, 'password');
return { success: false };
}
// Factor 2: TOTP
if (user.mfaEnabled) {
if (!totpCode) {
return { success: false, mfaRequired: true };
}
const verified = speakeasy.totp.verify({
secret: user.totpSecret,
encoding: 'base32',
token: totpCode
});
if (!verified) {
await this.logFailedAttempt(username, 'totp');
return { success: false };
}
}
// Factor 3: Device verification
if (deviceId) {
const knownDevice = await db.isKnownDevice(user.id, deviceId);
if (!knownDevice) {
await this.sendDeviceVerificationEmail(user, deviceId);
return { success: false, deviceVerificationRequired: true };
}
}
// Factor 4: Risk-based analysis
const risk = await this.riskService.analyze(user, credentials);
if (risk.score > 0.7) {
return {
success: false,
additionalVerificationRequired: true,
riskFactors: risk.factors
};
}
return { success: true, user };
}
// SAFE: Sensitive action requires re-authentication
async transferFunds(session, transferData) {
const { amount, toAccount, password, totpCode } = transferData;
const user = session.user;
// Re-verify password
if (!await bcrypt.compare(password, user.passwordHash)) {
throw new AuthError('Invalid password');
}
// Re-verify MFA
if (user.mfaEnabled) {
const verified = speakeasy.totp.verify({
secret: user.totpSecret,
encoding: 'base32',
token: totpCode
});
if (!verified) {
throw new AuthError('Invalid MFA code');
}
}
// Proceed with transfer
await this.executeTransfer(user, amount, toAccount);
}
}
Exploited in the Wild
Credential Stuffing
Password-only auth vulnerable to stuffed credentials.
SIM Swapping
SMS-only 2FA bypassed via carrier social engineering.
Session Hijacking
Token-only auth compromised via XSS.
Tools to test/exploit
-
Credential testing tools (Hydra, Medusa).
-
Session hijacking tools.
-
MFA bypass techniques.
CVE Examples
-
Account takeovers through single-factor systems.
-
API breaches via exposed API keys.
References
-
MITRE. "CWE-654: Reliance on a Single Factor in a Security Decision." https://cwe.mitre.org/data/definitions/654.html
-
NIST SP 800-63B. "Digital Identity Guidelines."