diff --git a/passwd/change-passwd.pl b/passwd/change-passwd.pl index 3f9b6864b..4fc5119f9 100755 --- a/passwd/change-passwd.pl +++ b/passwd/change-passwd.pl @@ -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 = ; $old =~ s/\r|\n//g; - &unix_crypt($old, $user->{'pass'}) eq $user->{'pass'} || + &useradmin::validate_password($old, $user->{'pass'}) || &errordie("Old password is incorrect"); } diff --git a/passwd/change_passwd.cgi b/passwd/change_passwd.cgi new file mode 100755 index 000000000..065d5a4b3 --- /dev/null +++ b/passwd/change_passwd.cgi @@ -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); +} diff --git a/passwd/passwd-lib.pl b/passwd/passwd-lib.pl index d14c1c9d8..c90496f03 100755 --- a/passwd/passwd-lib.pl +++ b/passwd/passwd-lib.pl @@ -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;