-
Notifications
You must be signed in to change notification settings - Fork 185
Replace insecure SHA1 password hashing with bcrypt #2275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Add Authen::Passphrase and Authen::Passphrase::BlowfishCrypt imports - Implement _hash_password() using bcrypt with cost factor 12 - Implement _verify_password() supporting both bcrypt and legacy SHA1 hashes - Implement _needs_password_upgrade() to detect legacy hashes - Update add_user() to hash new passwords with bcrypt - Update change_password() to use bcrypt hashing - Update login() to verify passwords with both hash types and auto-migrate SHA1 to bcrypt on successful login - Update compare_password() to use new verification function - Maintains backwards compatibility with existing SHA1 hashed passwords Co-authored-by: frankiejol <136835+frankiejol@users.noreply.github.com>
- Fix password validation to check for defined and non-empty values - Add constant-time comparison for legacy SHA1 hashes to prevent timing attacks - Add error handling for password upgrade during login (logs warning but doesn't break login) - Update in-memory password hash after change_password to maintain consistency Co-authored-by: frankiejol <136835+frankiejol@users.noreply.github.com>
- Replace bitwise AND comparison with XOR-based constant-time comparison to prevent timing leaks - Always compare full byte sequences regardless of length differences - Sanitize error messages in password upgrade logging to avoid exposing sensitive information Co-authored-by: frankiejol <136835+frankiejol@users.noreply.github.com>
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR addresses a critical security vulnerability by replacing insecure SHA1 password hashing with bcrypt. The implementation provides transparent migration from SHA1 to bcrypt on user login, maintaining backwards compatibility with existing passwords.
Changes:
- Introduced bcrypt password hashing with cost factor 12 for new passwords and password changes
- Added automatic migration of SHA1 passwords to bcrypt upon successful login
- Implemented constant-time comparison for legacy SHA1 password verification to prevent timing attacks
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| my $ppr = Authen::Passphrase::BlowfishCrypt->new( | ||
| cost => 12, |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The bcrypt cost factor is hardcoded to 12. While this is a reasonable default for 2025, it may become too weak as computing power increases, or may be too slow for some deployment environments. Consider making this configurable through a configuration file or environment variable to allow administrators to adjust it based on their security requirements and performance constraints.
This is a minor enhancement suggestion and not critical for the initial implementation.
| my $ppr = Authen::Passphrase::BlowfishCrypt->new( | |
| cost => 12, | |
| # Allow configuring bcrypt cost via environment variable, defaulting to 12. | |
| my $cost = 12; | |
| if ( defined $ENV{RAVADA_BCRYPT_COST} ) { | |
| my $env_cost = $ENV{RAVADA_BCRYPT_COST}; | |
| # Accept only integer values in a reasonable bcrypt range (4–31). | |
| if ( $env_cost =~ /^\d+$/ && $env_cost >= 4 && $env_cost <= 31 ) { | |
| $cost = $env_cost; | |
| } | |
| } | |
| my $ppr = Authen::Passphrase::BlowfishCrypt->new( | |
| cost => $cost, |
| # Migrate old SHA1 hash to bcrypt on successful login | ||
| if (_needs_password_upgrade($found->{password})) { | ||
| eval { | ||
| my $new_hash = _hash_password($password); | ||
| my $update_sth = $$CON->dbh->prepare( | ||
| "UPDATE users SET password=? WHERE id=?"); | ||
| $update_sth->execute($new_hash, $found->{id}); | ||
| $update_sth->finish; | ||
| }; | ||
| # Log upgrade failure but don't break login flow | ||
| carp "Warning: Failed to upgrade password hash for user $name" if $@; | ||
| } |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The password hash upgrade logic has a data consistency issue. After successfully upgrading the password in the database, the in-memory $self->{_data}->{password} still contains the old SHA1 hash because it was set at line 382 before the upgrade. This creates an inconsistency between the database and in-memory state.
Additionally, the statement handle should be properly finished to prevent resource leaks.
Recommendation: After successfully updating the database, unlock the hash, update the password field with the new hash, and re-lock it. Also add $update_sth->finish; after the execute call.
| return 0; | ||
| } | ||
| my $stored_hash = $sth->fetchrow(); | ||
|
|
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The compare_password function lacks error handling for the case where the user doesn't exist in the database. When fetchrow() returns undef (no matching user found), this will be passed to _verify_password() as the $stored_hash parameter. While _verify_password() has a check for undefined $stored_hash, it will throw an exception rather than returning false, which may not be the desired behavior for a comparison function.
Consider adding a check: return 0 if !defined $stored_hash; before calling _verify_password() to handle the case where the user doesn't exist more gracefully.
| return 0 if !defined $stored_hash; | |
| sub _verify_password { | ||
| my ($password, $stored_hash) = @_; | ||
|
|
||
| die "ERROR: password and hash required for verification\n" | ||
| if !defined $password || !defined $stored_hash; | ||
|
|
||
| # Check if this is a bcrypt hash (RFC 2307 format starting with {CRYPT}) | ||
| if ($stored_hash =~ /^\{CRYPT\}/) { | ||
| my $result; | ||
| eval { | ||
| my $ppr = Authen::Passphrase->from_rfc2307($stored_hash); | ||
| $result = $ppr->match($password); | ||
| }; | ||
| return 0 if $@; # Return false if hash parsing fails | ||
| return $result ? 1 : 0; | ||
| } |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The _verify_password function will attempt to verify passwords even for users with the special marker 'LK no pss' (used for external auth users without passwords). While this will correctly return false (since the marker won't match any password hash), it would be more explicit and efficient to check for this marker and return false immediately.
Consider adding: return 0 if $stored_hash eq '*LK* no pss'; at the beginning of the function to handle this case explicitly.
| # Migrate old SHA1 hash to bcrypt on successful login | ||
| if (_needs_password_upgrade($found->{password})) { | ||
| eval { | ||
| my $new_hash = _hash_password($password); | ||
| my $update_sth = $$CON->dbh->prepare( | ||
| "UPDATE users SET password=? WHERE id=?"); | ||
| $update_sth->execute($new_hash, $found->{id}); | ||
| $update_sth->finish; | ||
| }; | ||
| # Log upgrade failure but don't break login flow | ||
| carp "Warning: Failed to upgrade password hash for user $name" if $@; | ||
| } |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a potential race condition in the password migration logic. If a user logs in from multiple sessions simultaneously with an old SHA1 password, both sessions could attempt to upgrade the password hash at the same time. While this won't cause data corruption (both will write the same new hash), it could lead to unnecessary duplicate work and potential database contention.
This is a minor issue and probably acceptable given the transient nature of the migration period. However, if this becomes a concern, consider adding a flag or checking the hash again before updating to avoid redundant updates.
|
|
||
| # Legacy SHA1 support for backwards compatibility | ||
| # Use XOR-based constant-time comparison to prevent timing attacks | ||
| my $computed_hash = sha1_hex($password); |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_verify_password still uses unsalted sha1_hex for legacy password verification, so any accounts that have not yet been migrated to bcrypt remain vulnerable to fast offline cracking if the user database is compromised. An attacker who steals the current users table can efficiently recover these SHA1-based passwords using precomputed or GPU-accelerated attacks and reuse them across this and other services. To fully eliminate this risk, plan a forced rotation or proactive bulk rehash of remaining SHA1 password hashes to bcrypt rather than relying solely on opportunistic upgrade on login.
Addresses security vulnerability where passwords were stored using unsalted SHA1 hashes, making them vulnerable to rainbow table attacks and practical offline cracking if the database is compromised.
Implementation
Authen::Passphrase::BlowfishCryptwith cost factor 12 and random saltschange_password()Key Functions
Backwards Compatibility
All existing SHA1 passwords continue to work. No database migration or user intervention required.
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.