Not Using Password Aging

Description

Not Using Password Aging is a vulnerability that occurs when a product lacks mechanisms for managing password expiration and forced rotation. Password aging policies require users to change their passwords after a defined period (such as every 30, 60, or 90 days), limiting the window of opportunity for attackers who have obtained credentials. While modern security guidance has shifted away from mandatory password rotation due to concerns about password fatigue and weaker password choices, password aging remains relevant for compliance requirements (such as PCI DSS) and for high-security environments where credential compromise risks are elevated.

Risk

The absence of password aging mechanisms increases the risk window for credential compromise attacks. Passwords that never expire provide attackers unlimited time to crack obtained password hashes through brute force or dictionary attacks. Compromised credentials remain valid indefinitely, allowing persistent unauthorized access. In environments where password theft goes undetected, attackers maintain access until passwords are changed for other reasons. The risk is particularly acute for service accounts and administrative credentials that may not be subject to regular user-driven changes. However, it's important to note that modern security research suggests focusing on password strength, breach detection, and multi-factor authentication may be more effective than mandatory rotation.

Solution

Implement password aging policies appropriate to your security requirements and compliance obligations. Configure password expiration periods based on risk assessment - shorter periods (30-90 days) for high-privilege accounts and longer periods or no expiration for standard accounts with MFA enabled. Provide advance notification to users before password expiration. Implement account lockout or access restrictions when passwords expire. Maintain password history to prevent reuse of recent passwords. Consider alternatives to mandatory rotation such as compromised credential detection services, multi-factor authentication, and breach notification-triggered password resets. For compliance-driven requirements (PCI DSS, HIPAA), implement the minimum required rotation periods. Document exceptions for service accounts with appropriate compensating controls.

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Without password aging, compromised credentials remain valid indefinitely. Attackers who obtain passwords through theft, cracking, or social engineering maintain persistent access until the password is changed for other reasons, increasing the likelihood of privilege escalation and identity assumption.

Example Code

Vulnerable Code (Java/Spring Security)

The following code demonstrates a system without password aging:

@Configuration
public class VulnerableSecurityConfig {

    // Vulnerable: No password aging mechanism
    @Bean
    public UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) {
                User user = userRepository.findByUsername(username);

                // No check for password expiration
                return new org.springframework.security.core.userdetails.User(
                    user.getUsername(),
                    user.getPassword(),
                    true,  // enabled
                    true,  // accountNonExpired
                    true,  // credentialsNonExpired - ALWAYS TRUE!
                    true,  // accountNonLocked
                    user.getAuthorities()
                );
            }
        };
    }
}

@Entity
public class VulnerableUser {
    @Id
    private Long id;
    private String username;
    private String password;
    // No passwordLastChanged field
    // No passwordExpirationDate field
    // No mechanism to track or enforce password aging
}
-- Vulnerable: User table with no password aging fields
CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    username VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    -- No password_last_changed column
    -- No password_expires_at column
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Fixed Code (Java/Spring Security)

@Configuration
public class SecureSecurityConfig {

    @Value("${security.password.max-age-days:90}")
    private int passwordMaxAgeDays;

    @Bean
    public UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) {
                User user = userRepository.findByUsername(username);

                // Check if password has expired
                boolean credentialsNonExpired = !isPasswordExpired(user);

                return new org.springframework.security.core.userdetails.User(
                    user.getUsername(),
                    user.getPassword(),
                    user.isEnabled(),
                    true,  // accountNonExpired
                    credentialsNonExpired,  // Based on password age
                    !user.isLocked(),
                    user.getAuthorities()
                );
            }
        };
    }

    private boolean isPasswordExpired(User user) {
        if (user.getPasswordLastChanged() == null) {
            return true;  // Force change if never set
        }

        LocalDateTime expirationDate = user.getPasswordLastChanged()
            .plusDays(passwordMaxAgeDays);

        return LocalDateTime.now().isAfter(expirationDate);
    }
}

@Entity
public class SecureUser {
    @Id
    private Long id;
    private String username;
    private String password;

    // Password aging fields
    @Column(name = "password_last_changed")
    private LocalDateTime passwordLastChanged;

    @Column(name = "password_expires_at")
    private LocalDateTime passwordExpiresAt;

    @Column(name = "force_password_change")
    private boolean forcePasswordChange;

    // Track password history
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<PasswordHistory> passwordHistory;

    public void setPassword(String newPassword) {
        // Add current password to history before changing
        if (this.password != null) {
            PasswordHistory history = new PasswordHistory();
            history.setUser(this);
            history.setPasswordHash(this.password);
            history.setChangedAt(this.passwordLastChanged);
            passwordHistory.add(history);
        }

        this.password = newPassword;
        this.passwordLastChanged = LocalDateTime.now();
        this.passwordExpiresAt = LocalDateTime.now().plusDays(90);
        this.forcePasswordChange = false;
    }
}

@Service
public class PasswordAgingService {

    @Value("${security.password.warning-days:14}")
    private int warningDays;

    public void checkPasswordExpiration(User user) {
        if (user.getPasswordExpiresAt() == null) {
            return;
        }

        LocalDateTime now = LocalDateTime.now();
        LocalDateTime warningDate = user.getPasswordExpiresAt().minusDays(warningDays);

        if (now.isAfter(user.getPasswordExpiresAt())) {
            throw new CredentialsExpiredException("Password has expired");
        } else if (now.isAfter(warningDate)) {
            long daysRemaining = ChronoUnit.DAYS.between(now, user.getPasswordExpiresAt());
            // Notify user of impending expiration
            notificationService.sendPasswordExpirationWarning(user, daysRemaining);
        }
    }

    // Prevent password reuse
    public boolean isPasswordInHistory(User user, String newPassword, int historyCount) {
        return user.getPasswordHistory().stream()
            .sorted(Comparator.comparing(PasswordHistory::getChangedAt).reversed())
            .limit(historyCount)
            .anyMatch(h -> passwordEncoder.matches(newPassword, h.getPasswordHash()));
    }
}
-- Fixed: User table with password aging support
CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    username VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    password_last_changed TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    password_expires_at TIMESTAMP,
    force_password_change BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE password_history (
    id BIGINT PRIMARY KEY,
    user_id BIGINT REFERENCES users(id),
    password_hash VARCHAR(255) NOT NULL,
    changed_at TIMESTAMP NOT NULL
);

CREATE INDEX idx_password_expires ON users(password_expires_at);

The fix implements password expiration tracking, history to prevent reuse, warning notifications, and forced password change on expiration.


Exploited in the Wild

Long-Term Credential Theft Campaigns (APT Groups, Ongoing)

Advanced Persistent Threat groups have maintained access to compromised networks for months or years using stolen credentials that never expired. Without password aging forcing credential rotation, attackers with initial access through phishing or other means can establish persistent presence. Notable examples include APT29's SolarWinds campaign where stolen credentials remained valid throughout the intrusion.

Stale Service Account Compromise (Multiple Organizations, Ongoing)

Service accounts and application credentials without expiration policies have been primary targets for attackers. Once compromised, these credentials provide indefinite access since they are rarely changed. Security audits frequently discover service accounts with passwords unchanged for years.

PCI DSS Compliance Violations (Retail Sector, Multiple Years)

Organizations failing to implement password aging as required by PCI DSS have faced breaches where compromised payment system credentials remained valid for extended periods. Compliance audits have identified password aging gaps as contributing factors in breach investigations.


Tools to Test/Exploit

  • CrackMapExec — Tests for password reuse and identifies accounts with long-unchanged passwords.

  • BloodHound — Active Directory analysis tool that identifies accounts with passwords that haven't changed in extended periods.

  • ADRecon — Generates reports on Active Directory password policies and identifies accounts not complying with aging requirements.


CVE Examples

  • CVE-2019-5544 — VMware ESXi OpenSLP service used credentials without expiration, enabling persistent access after compromise.

  • CVE-2020-1472 — Zerologon vulnerability exploitation was prolonged by lack of credential rotation forcing password changes.


References

  1. MITRE Corporation. "CWE-262: Not Using Password Aging." Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/262.html

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

  3. PCI Security Standards Council. "PCI DSS Requirements 8.2.4 - Password/Passphrase Changes." https://www.pcisecuritystandards.org/

  4. Microsoft. "Password Guidance." https://www.microsoft.com/en-us/research/publication/password-guidance/