Use of Single-factor Authentication
Description
Use of Single-factor Authentication is a vulnerability that occurs when a product relies on only one authentication factor (typically a password) in contexts where multiple authentication factors are required for adequate security. Authentication factors are categorized as: something you know (passwords, PINs), something you have (tokens, smart cards, phones), and something you are (biometrics). While single-factor authentication may be appropriate for low-risk applications, sensitive systems handling financial data, healthcare information, administrative access, or personal data require multiple independent factors to provide adequate protection against credential theft and unauthorized access.
Risk
Single-factor authentication, particularly password-only systems, represents a significant security weakness because compromise of that single factor grants complete account access. Passwords are vulnerable to numerous attack vectors: phishing, credential stuffing, brute force, keyloggers, shoulder surfing, and data breaches exposing credential databases. Once a password is compromised, attackers have unrestricted access to the account regardless of how strong other security measures may be. The risk is amplified by widespread password reuse - a breach at one service often compromises accounts across multiple services. High-value targets like administrative accounts, financial systems, and healthcare applications are particularly vulnerable, as attackers specifically target these for their potential impact.
Solution
Implement multi-factor authentication (MFA) for all sensitive access and critical functions. Require at least two independent factors from different categories - typically something you know combined with something you have. Deploy time-based one-time passwords (TOTP), hardware security keys (FIDO2/WebAuthn), push notifications, or SMS codes as second factors, with preference for phishing-resistant methods like hardware keys. Apply risk-based authentication that escalates requirements based on login context (new device, unusual location, sensitive operation). Ensure MFA cannot be disabled through simple account settings without additional verification. Consider passwordless authentication using hardware tokens or biometrics for highest-security scenarios. Provide fallback mechanisms that maintain security while allowing account recovery.
Common Consequences
| Impact | Details |
|---|---|
| Access Control | Scope: Access Control If the single authentication secret is compromised, attackers gain full access to the account. No additional barriers prevent unauthorized access once the password is known. |
| Integrity, Confidentiality | Scope: Integrity, Confidentiality Complete account compromise enables data theft, unauthorized transactions, impersonation, and potential lateral movement to other systems using harvested credentials. |
Example Code
Vulnerable Code (Java)
The following examples demonstrate single-factor authentication implementations:
// Vulnerable: Password-only authentication for sensitive system
import java.security.MessageDigest;
import javax.servlet.http.*;
public class VulnerableBankingAuth extends HttpServlet {
protected void doPost(HttpServletRequest request,
HttpServletResponse response) {
String username = request.getParameter("username");
String password = request.getParameter("password");
// Vulnerable: Only password authentication for banking system
if (authenticateUser(username, password)) {
// Full access granted with just a password!
HttpSession session = request.getSession();
session.setAttribute("authenticated", true);
session.setAttribute("username", username);
// Can now access sensitive banking functions
response.sendRedirect("/dashboard");
} else {
response.sendRedirect("/login?error=invalid");
}
}
// Vulnerable: Weak hash without salt
private boolean authenticateUser(String username, String password) {
User user = userRepository.findByUsername(username);
if (user == null) return false;
// Also vulnerable: SHA-1 is weak
String hashedInput = sha1Hash(password);
return hashedInput.equals(user.getPasswordHash());
}
private String sha1Hash(String input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] hash = md.digest(input.getBytes());
return bytesToHex(hash);
} catch (Exception e) {
return null;
}
}
}
# Vulnerable: Admin portal with only password authentication
from flask import Flask, request, session, redirect
app = Flask(__name__)
@app.route('/admin/login', methods=['POST'])
def admin_login():
username = request.form.get('username')
password = request.form.get('password')
# Vulnerable: Admin access with just password
admin = Admin.query.filter_by(username=username).first()
if admin and admin.check_password(password):
# Full admin access with single factor!
session['admin_authenticated'] = True
session['admin_id'] = admin.id
session['admin_role'] = admin.role # Could be superadmin!
return redirect('/admin/dashboard')
return redirect('/admin/login?error=1')
# Vulnerable: Sensitive operations without second factor
@app.route('/admin/users/delete/<user_id>', methods=['POST'])
def delete_user(user_id):
# Only checks session, no additional verification
if session.get('admin_authenticated'):
User.query.filter_by(id=user_id).delete()
db.session.commit()
return redirect('/admin/users')
return redirect('/admin/login')
// Vulnerable: C authentication with password only
#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>
typedef struct {
char username[64];
char password_hash[65];
int privilege_level; // 0=user, 1=admin, 2=superadmin
} UserRecord;
// Vulnerable: Returns full access including admin with just password
int vulnerable_authenticate(const char *username, const char *password) {
UserRecord *user = lookup_user(username);
if (user == NULL) {
return -1; // User not found
}
char input_hash[65];
compute_sha256(password, input_hash);
// Vulnerable: Password alone grants any privilege level
if (strcmp(input_hash, user->password_hash) == 0) {
return user->privilege_level; // Returns 2 for superadmin!
}
return -1; // Auth failed
}
int main() {
char username[64], password[64];
printf("Username: ");
scanf("%63s", username);
printf("Password: ");
scanf("%63s", password);
int level = vulnerable_authenticate(username, password);
if (level >= 0) {
// Vulnerable: Full access with single factor
if (level == 2) {
printf("Welcome, Superadmin!\n");
superadmin_menu(); // Full system control!
}
}
return 0;
}
Fixed Code (Java)
// Fixed: Multi-factor authentication for sensitive system
import java.security.SecureRandom;
import javax.servlet.http.*;
public class SecureBankingAuth extends HttpServlet {
private final TOTPService totpService;
private final MFAEnrollmentService mfaService;
protected void doPost(HttpServletRequest request,
HttpServletResponse response) {
String action = request.getParameter("action");
if ("password".equals(action)) {
handlePasswordStep(request, response);
} else if ("mfa".equals(action)) {
handleMFAStep(request, response);
}
}
// Fixed: Password is just first factor
private void handlePasswordStep(HttpServletRequest request,
HttpServletResponse response) {
String username = request.getParameter("username");
String password = request.getParameter("password");
if (authenticatePassword(username, password)) {
// Fixed: Password verified, but not fully authenticated yet
HttpSession session = request.getSession();
session.setAttribute("pending_mfa", true);
session.setAttribute("pending_username", username);
session.setMaxInactiveInterval(300); // 5 min to complete MFA
// Fixed: Redirect to MFA step
response.sendRedirect("/login/mfa");
} else {
response.sendRedirect("/login?error=invalid");
}
}
// Fixed: Second factor required
private void handleMFAStep(HttpServletRequest request,
HttpServletResponse response) {
HttpSession session = request.getSession(false);
// Verify first factor was completed
if (session == null || !Boolean.TRUE.equals(session.getAttribute("pending_mfa"))) {
response.sendRedirect("/login");
return;
}
String username = (String) session.getAttribute("pending_username");
String totpCode = request.getParameter("totp_code");
// Fixed: Verify TOTP second factor
if (totpService.verifyCode(username, totpCode)) {
// Clear pending state
session.removeAttribute("pending_mfa");
session.removeAttribute("pending_username");
// Fixed: Only now is user fully authenticated
session.setAttribute("authenticated", true);
session.setAttribute("username", username);
session.setAttribute("auth_time", System.currentTimeMillis());
auditService.logSuccessfulMFA(username);
response.sendRedirect("/dashboard");
} else {
auditService.logFailedMFA(username);
// Don't reveal which factor failed
session.invalidate();
response.sendRedirect("/login?error=invalid");
}
}
// Fixed: Strong password hashing with salt
private boolean authenticatePassword(String username, String password) {
User user = userRepository.findByUsername(username);
// Constant-time check to prevent enumeration
if (user == null) {
passwordEncoder.encode(password); // Dummy operation
return false;
}
// Using bcrypt/argon2 for password verification
return passwordEncoder.matches(password, user.getPasswordHash());
}
}
// Fixed: TOTP verification service
@Service
public class TOTPService {
private static final int TIME_STEP_SECONDS = 30;
private static final int CODE_DIGITS = 6;
public boolean verifyCode(String username, String code) {
User user = userRepository.findByUsername(username);
if (user == null || user.getTotpSecret() == null) {
return false;
}
// Allow for clock drift (1 step before and after)
long currentTime = System.currentTimeMillis() / 1000;
for (int i = -1; i <= 1; i++) {
String expectedCode = generateTOTP(
user.getTotpSecret(),
(currentTime / TIME_STEP_SECONDS) + i
);
if (MessageDigest.isEqual(code.getBytes(), expectedCode.getBytes())) {
// Prevent code reuse
if (isCodeAlreadyUsed(username, code)) {
return false;
}
markCodeUsed(username, code);
return true;
}
}
return false;
}
}
# Fixed: Multi-factor authentication for admin portal
from flask import Flask, request, session, redirect
import pyotp
from functools import wraps
app = Flask(__name__)
def require_mfa(f):
"""Decorator requiring completed MFA."""
@wraps(f)
def decorated(*args, **kwargs):
if not session.get('mfa_verified'):
return redirect('/admin/login')
return f(*args, **kwargs)
return decorated
def require_step_up(f):
"""Decorator requiring step-up authentication for sensitive ops."""
@wraps(f)
def decorated(*args, **kwargs):
# Check if step-up auth was recently completed
step_up_time = session.get('step_up_time', 0)
if time.time() - step_up_time > 300: # 5 minute window
session['pending_action'] = request.url
return redirect('/admin/step-up')
return f(*args, **kwargs)
return decorated
@app.route('/admin/login', methods=['POST'])
def admin_login():
step = request.form.get('step', 'password')
if step == 'password':
username = request.form.get('username')
password = request.form.get('password')
admin = Admin.query.filter_by(username=username).first()
if admin and admin.check_password(password):
# Fixed: Password verified, require MFA
session['pending_admin'] = admin.id
session['pending_username'] = username
return redirect('/admin/mfa')
return redirect('/admin/login?error=1')
elif step == 'mfa':
if 'pending_admin' not in session:
return redirect('/admin/login')
totp_code = request.form.get('totp_code')
admin = Admin.query.get(session['pending_admin'])
if admin and verify_totp(admin.totp_secret, totp_code):
# Fixed: Both factors verified
session.pop('pending_admin')
session.pop('pending_username')
session['admin_authenticated'] = True
session['admin_id'] = admin.id
session['mfa_verified'] = True
session['auth_time'] = time.time()
return redirect('/admin/dashboard')
return redirect('/admin/mfa?error=1')
# Fixed: Sensitive operations require step-up authentication
@app.route('/admin/users/delete/<user_id>', methods=['POST'])
@require_mfa
@require_step_up # Requires re-authentication
def delete_user(user_id):
# Additional verification for destructive action
User.query.filter_by(id=user_id).delete()
db.session.commit()
audit_log.record('user_deleted', user_id, session['admin_id'])
return redirect('/admin/users')
def verify_totp(secret, code):
"""Verify TOTP code with clock drift allowance."""
totp = pyotp.TOTP(secret)
return totp.verify(code, valid_window=1)
The fix implements multi-factor authentication requiring both password and TOTP verification.
Exploited in the Wild
MFA Bypass in Chat Application (2022)
CVE-2022-35248 documented a chat application where the second factor of two-factor authentication was skipped when Central Authentication Service (CAS) was enabled, effectively reducing security to single-factor.
Credential Stuffing Attacks (Ongoing)
Numerous high-profile breaches have exploited single-factor authentication, using credentials from other data breaches to gain unauthorized access to accounts protected only by passwords.
Tools to Test/Exploit
-
Credential Stuffing Tools — Tools that test for single-factor vulnerabilities using leaked credentials.
-
Burp Suite — Web security tool for testing authentication mechanisms.
-
MFA Configuration Scanners — Tools for assessing MFA implementation and coverage.
CVE Examples
-
CVE-2022-35248 — MFA second factor skipped when CAS enabled.
-
CVE-2020-12812 — MFA bypass through API endpoint.
-
CVE-2018-0114 — JWT authentication bypass in web application.
References
-
MITRE Corporation. "CWE-308: Use of Single-factor Authentication." Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/308.html
-
NIST. "Digital Identity Guidelines: Authentication and Lifecycle Management." SP 800-63B. https://pages.nist.gov/800-63-3/sp800-63b.html
-
OWASP Foundation. "Multifactor Authentication Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html