Start of work on password change API for use by roundcube / etc

This commit is contained in:
Jamie Cameron
2021-08-01 11:11:22 -07:00
parent ab62ddca12
commit fa5fc3a94b
3 changed files with 100 additions and 2 deletions

View File

@ -29,10 +29,11 @@ $user || &errordie("User $ARGV[0] does not exist");
$| = 1;
if ($askold) {
# Ask for the old password
&foreign_require("useradmin");
print "(current) UNIX password: ";
$old = <STDIN>;
$old =~ s/\r|\n//g;
&unix_crypt($old, $user->{'pass'}) eq $user->{'pass'} ||
&useradmin::validate_password($old, $user->{'pass'}) ||
&errordie("Old password is incorrect");
}

51
passwd/change_passwd.cgi Executable file
View File

@ -0,0 +1,51 @@
#!/usr/local/bin/perl
# Change a user's password knowing the old one. For user only via anonymous
# API calls.
require './passwd-lib.pl';
&ReadParse();
print "Content-type: text/plain\n\n";
# Validate inputs
my $err = &apply_rate_limit($ENV{'REMOTE_ADDR'});
&error_exit($err) if ($err);
$in{'user'} || &error_exit("Missing user parameter");
$in{'old'} || &error_exit("Missing old parameter");
$in{'new'} || &error_exit("Missing new parameter");
$ENV{'ANONYMOUS_USER'} || &error_exit("Can only be called in anonymous mode");
$ENV{'REQUEST_METHOD'} eq 'POST' ||
&error_exit("Passwords can only be submitted via POST");
&foreign_installed("useradmin") ||
&error_exit("Users and Groups module is not supported on this OS");
# Validate user and pass
my $err = &apply_rate_limit($in{'user'});
&error_exit($err) if ($err);
&foreign_require("useradmin");
my $user = &find_user($in{'user'});
$user || &error_exit("User does not exist");
&useradmin::validate_password($in{'old'}, $user->{'pass'}) ||
&error_exit("Incorrect password");
my $err = &useradmin::check_password_restrictions(
$in{'pass'}, $in{'user'}, $user);
&error_exit("Invalid password : $err") if ($err);
# Do the change
&clear_rate_limit($ENV{'REMOTE_ADDR'});
&clear_rate_limit($in{'user'});
eval {
local $main::error_must_die = 1;
&change_password($user, $in{'pass'}, 1);
};
if ($@) {
&error_exit($@);
}
else {
print "OK: Password changed for $in{'user'}\n";
}
sub error_exit
{
print "FAILED: ",join("", @_),"\n";
exit(0);
}

View File

@ -14,6 +14,9 @@ BEGIN { push(@INC, ".."); };
use WebminCore;
&init_config();
%access = &get_module_acl();
$rate_limit_file = "$module_var_directory/rate-limit";
$rate_limit_timeout = 10*60; # 10 minutes
$rate_limit_max = 10;
=head2 can_edit_passwd(&user)
@ -80,7 +83,6 @@ sub find_user
local $mod;
foreach $mod ([ "useradmin", "user-lib.pl" ],
[ "ldap-useradmin", "ldap-useradmin-lib.pl" ],
# [ "nis", "nis-lib.pl" ],
) {
next if (!&foreign_installed($mod->[0], 1));
&foreign_require($mod->[0], $mod->[1]);
@ -146,5 +148,49 @@ if ($others) {
}
}
# apply_rate_limit(key)
# Delays for some amount of time based on the key, to prevent brute force attacks
sub apply_rate_limit
{
my ($key) = @_;
my $now = time();
my %rate;
&lock_file($rate_limit_file);
&read_file($rate_limit_file, \%rate);
$rate{$key."_last"} ||= $now;
if ($now - $rate{$key."_last"} > $rate_limit_timeout) {
# Time since blocking for this key started as expired
delete($rate{$key});
delete($rate{$key."_last"});
}
my $rv;
if ($rate{$key} > $rate_limit_max) {
$rv = "Too many failures for $key";
}
else {
sleep($rate{$key} ** 2);
$rate{$key}++;
}
&write_file($rate_limit_file, \%rate);
&unlock_file($rate_limit_file);
return $rv;
}
# clear_rate_limit(key)
# After a successful operation, clear any rate limits for the given key
sub clear_rate_limit
{
my ($key) = @_;
my %rate;
&lock_file($rate_limit_file);
&read_file($rate_limit_file, \%rate);
delete($rate{$key});
delete($rate{$key."_last"});
&write_file($rate_limit_file, \%rate);
&unlock_file($rate_limit_file);
}
1;