Koha/opac/opac-password-recovery.pl
Fridolin Somers 904e926ba0 Bug 26904: OPAC password recovery allows regexp in email
When using OPAC password recovery form, opac/opac-password-recovery.pl :
if one provides correct login and an email, there is a check that this email is one of patron's.

This check uses RegExp with case insensitive :
  if ( $email && !( any { /^$email$/i } @emails ) )

This is a security issue since one can simply enter '.*'.
Severity is normal because the login must be a correct.

I propose to use simple string compare with lowercase to be case insensitive.

Test plan :
1) Don't apply patch
2) Enable system preference 'OpacResetPassword'
3) Go to 'OPAC > Log in to your account > Forgot your password?'
4) Enter an existing userid or cardnumber and '.*' in 'Email'
5) The password recovery is created ! (check table 'borrower_password_recovery')
6) Apply patch
7) Enter an existing userid or cardnumber and '.*' in 'Email'
8) You get the message 'No account was found with the provided information.'
9) Enter an existing userid or cardnumber and in 'Email' the corresponding email but with different case
10) The password recovery is created (check table 'borrower_password_recovery')

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Julian Maurice <julian.maurice@biblibre.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
2020-11-09 16:18:25 +01:00

225 lines
7.6 KiB
Perl
Executable file

#!/usr/bin/perl
use Modern::Perl;
use CGI;
use C4::Auth;
use C4::Koha;
use C4::Output;
use C4::Context;
use Koha::Patron::Password::Recovery
qw(SendPasswordRecoveryEmail ValidateBorrowernumber GetValidLinkInfo CompletePasswordRecovery DeleteExpiredPasswordRecovery);
use Koha::Patrons;
use Koha::Patrons;
my $query = CGI->new;
use HTML::Entities;
use Try::Tiny;
use List::Util qw/any/;
my ( $template, $dummy, $cookie ) = get_template_and_user(
{
template_name => "opac-password-recovery.tt",
query => $query,
type => "opac",
authnotrequired => 1,
debug => 1,
}
);
my $email = $query->param('email') // q{};
my $password = $query->param('newPassword');
my $repeatPassword = $query->param('repeatPassword');
my $id = $query->param('id');
my $uniqueKey = $query->param('uniqueKey');
my $username = $query->param('username') // q{};
my $borrower_number;
#errors
my $hasError;
#email form error
my $errNoBorrowerFound;
my $errNoBorrowerEmail;
my $errMultipleAccountsForEmail;
my $errAlreadyStartRecovery;
my $errTooManyEmailFound;
my $errBadEmail;
my $errResetForbidden;
#new password form error
my $errLinkNotValid;
if ( $query->param('sendEmail') || $query->param('resendEmail') ) {
#try with the main email
my $borrower;
my $search_results;
# Find the borrower by userid, card number, or email
if ($username) {
$search_results = Koha::Patrons->search( { -or => { userid => $username, cardnumber => $username }, login_attempts => { '!=', Koha::Patron::ADMINISTRATIVE_LOCKOUT } } );
}
elsif ($email) {
$search_results = Koha::Patrons->search( { -or => { email => $email, emailpro => $email, B_email => $email }, login_attempts => { '!=', Koha::Patron::ADMINISTRATIVE_LOCKOUT } } );
}
if ( !defined $search_results || $search_results->count < 1) {
$hasError = 1;
$errNoBorrowerFound = 1;
}
elsif ( $username && $search_results->count > 1) { # Multiple accounts for username
$hasError = 1;
$errNoBorrowerFound = 1;
}
elsif ( $email && $search_results->count > 1) { # Muliple accounts for E-Mail
$hasError = 1;
$errMultipleAccountsForEmail = 1;
}
elsif ( $borrower = $search_results->next() ) { # One matching borrower
if ( $borrower->category->effective_reset_password ) {
my @emails = grep { $_ } ( $borrower->email, $borrower->emailpro, $borrower->B_email );
my $firstNonEmptyEmail;
$firstNonEmptyEmail = $emails[0] if @emails;
# Is the given email one of the borrower's ?
if ( $email && !( any { lc($_) eq lc($email) } @emails ) ) {
$hasError = 1;
$errNoBorrowerFound = 1;
}
# If there is no given email, and there is no email on record
elsif ( !$email && !$firstNonEmptyEmail ) {
$hasError = 1;
$errNoBorrowerEmail = 1;
}
# Check if a password reset already issued for this
# borrower AND we are not asking for a new email
elsif ( not $query->param('resendEmail') ) {
if ( ValidateBorrowernumber( $borrower->borrowernumber ) ) {
$hasError = 1;
$errAlreadyStartRecovery = 1;
}
else {
DeleteExpiredPasswordRecovery( $borrower->borrowernumber );
}
}
# Set the $email, if we don't have one.
if ( !$hasError && !$email ) {
$email = $firstNonEmptyEmail;
}
}
else {
$hasError = 1;
$errResetForbidden = 1;
}
}
else { # 0 matching borrower
$hasError = 1;
$errNoBorrowerFound = 1;
}
if ($hasError) {
$template->param(
hasError => 1,
errNoBorrowerFound => $errNoBorrowerFound,
errTooManyEmailFound => $errTooManyEmailFound,
errAlreadyStartRecovery => $errAlreadyStartRecovery,
errBadEmail => $errBadEmail,
errNoBorrowerEmail => $errNoBorrowerEmail,
errMultipleAccountsForEmail => $errMultipleAccountsForEmail,
errResetForbidden => $errResetForbidden,
password_recovery => 1,
email => HTML::Entities::encode($email),
username => $username
);
}
elsif ( SendPasswordRecoveryEmail( $borrower, $email, scalar $query->param('resendEmail') ) ) { # generate uuid and send recovery email
$template->param(
mail_sent => 1,
email => $email
);
}
else { # if it doesn't work....
$template->param(
hasError => 1,
password_recovery => 1,
sendmailError => 1
);
}
}
elsif ( $query->param('passwordReset') ) {
( $borrower_number, $username ) = GetValidLinkInfo($uniqueKey);
my $error;
my $min_password_length = C4::Context->preference('minPasswordPreference');
my $require_strong_password = C4::Context->preference('RequireStrongPassword');
if ( not $borrower_number ) {
$error = 'errLinkNotValid';
} elsif ( $password ne $repeatPassword ) {
$error = 'errPassNotMatch';
} else {
my $borrower = Koha::Patrons->find($borrower_number);
$min_password_length = $borrower->category->effective_min_password_length;
$require_strong_password = $borrower->category->effective_require_strong_password;
try {
$borrower->set_password({ password => $password });
CompletePasswordRecovery($uniqueKey);
$template->param(
password_reset_done => 1,
username => $username
);
}
catch {
if ( $_->isa('Koha::Exceptions::Password::TooShort') ) {
$error = 'password_too_short';
}
elsif ( $_->isa('Koha::Exceptions::Password::WhitespaceCharacters') ) {
$error = 'password_has_whitespaces';
}
elsif ( $_->isa('Koha::Exceptions::Password::TooWeak') ) {
$error = 'password_too_weak';
}
};
}
if ( $error ) {
$template->param(
new_password => 1,
email => $email,
uniqueKey => $uniqueKey,
hasError => 1,
$error => 1,
minPasswordLength => $min_password_length,
RequireStrongPassword => $require_strong_password
);
}
}
elsif ($uniqueKey) { #reset password form
#check if the link is valid
( $borrower_number, $username ) = GetValidLinkInfo($uniqueKey);
if ( !$borrower_number ) {
$errLinkNotValid = 1;
}
my $borrower = Koha::Patrons->find($borrower_number);
$template->param(
new_password => 1,
email => $email,
uniqueKey => $uniqueKey,
username => $username,
errLinkNotValid => $errLinkNotValid,
hasError => ( $errLinkNotValid ? 1 : 0 ),
minPasswordLength => $borrower->category->effective_min_password_length,
RequireStrongPassword => $borrower->category->effective_require_strong_password
);
}
else { #password recovery form (to send email)
$template->param( password_recovery => 1 );
}
output_html_with_http_headers $query, $cookie, $template->output;