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

ImpactDetails
Access ControlScope: 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, ConfidentialityScope: 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


CVE Examples


References

  1. MITRE Corporation. "CWE-308: Use of Single-factor Authentication." Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/308.html

  2. NIST. "Digital Identity Guidelines: Authentication and Lifecycle Management." SP 800-63B. https://pages.nist.gov/800-63-3/sp800-63b.html

  3. OWASP Foundation. "Multifactor Authentication Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html