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
| Impact | Details |
|---|---|
| Access Control | Scope: 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
-
MITRE Corporation. "CWE-262: Not Using Password Aging." Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/262.html
-
NIST. "Digital Identity Guidelines: Authentication and Lifecycle Management." SP 800-63B. https://pages.nist.gov/800-63-3/sp800-63b.html
-
PCI Security Standards Council. "PCI DSS Requirements 8.2.4 - Password/Passphrase Changes." https://www.pcisecuritystandards.org/
-
Microsoft. "Password Guidance." https://www.microsoft.com/en-us/research/publication/password-guidance/