From aa45cf148a053f89455bbf0652a2981da0731714 Mon Sep 17 00:00:00 2001 From: Christophe Maudoux Date: Wed, 12 Dec 2018 23:51:33 +0100 Subject: [PATCH] Append bruteForce Protection number of allowed failed Login parameter (#1506) --- .../Lemonldap/NG/Common/Conf/DefaultValues.pm | 51 ++++---- .../lib/Lemonldap/NG/Manager/Attributes.pm | 4 + .../Lemonldap/NG/Manager/Build/Attributes.pm | 8 +- .../NG/Portal/Plugins/BruteForceProtection.pm | 31 +++-- .../t/61-BruteForceProtection.t | 111 +++++++++++++++--- 5 files changed, 149 insertions(+), 56 deletions(-) diff --git a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/DefaultValues.pm b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/DefaultValues.pm index be740fee7..daa2e3caf 100644 --- a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/DefaultValues.pm +++ b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/DefaultValues.pm @@ -15,31 +15,32 @@ sub defaultValues { 'type' => 'category' } }, - 'authChoiceParam' => 'lmAuth', - 'authentication' => 'Demo', - 'available2F' => 'UTOTP,TOTP,U2F,REST,Ext2F,Yubikey', - 'available2FSelfRegistration' => 'TOTP,U2F,Yubikey', - 'bruteForceProtectionMaxAge' => 300, - 'bruteForceProtectionTempo' => 30, - 'captcha_mail_enabled' => 1, - 'captcha_register_enabled' => 1, - 'captcha_size' => 6, - 'casAccessControlPolicy' => 'none', - 'casAuthnLevel' => 1, - 'checkTime' => 600, - 'checkXSS' => 1, - 'confirmFormMethod' => 'post', - 'cookieName' => 'lemonldap', - 'cspConnect' => '\'self\'', - 'cspDefault' => '\'self\'', - 'cspFont' => '\'self\'', - 'cspFormAction' => '\'self\'', - 'cspImg' => '\'self\' data:', - 'cspScript' => '\'self\'', - 'cspStyle' => '\'self\'', - 'dbiAuthnLevel' => 2, - 'dbiExportedVars' => {}, - 'demoExportedVars' => { + 'authChoiceParam' => 'lmAuth', + 'authentication' => 'Demo', + 'available2F' => 'UTOTP,TOTP,U2F,REST,Ext2F,Yubikey', + 'available2FSelfRegistration' => 'TOTP,U2F,Yubikey', + 'bruteForceProtectionMaxAge' => 300, + 'bruteForceProtectionMaxFailed' => 3, + 'bruteForceProtectionTempo' => 30, + 'captcha_mail_enabled' => 1, + 'captcha_register_enabled' => 1, + 'captcha_size' => 6, + 'casAccessControlPolicy' => 'none', + 'casAuthnLevel' => 1, + 'checkTime' => 600, + 'checkXSS' => 1, + 'confirmFormMethod' => 'post', + 'cookieName' => 'lemonldap', + 'cspConnect' => '\'self\'', + 'cspDefault' => '\'self\'', + 'cspFont' => '\'self\'', + 'cspFormAction' => '\'self\'', + 'cspImg' => '\'self\' data:', + 'cspScript' => '\'self\'', + 'cspStyle' => '\'self\'', + 'dbiAuthnLevel' => 2, + 'dbiExportedVars' => {}, + 'demoExportedVars' => { 'cn' => 'cn', 'mail' => 'mail', 'uid' => 'uid' diff --git a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Attributes.pm b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Attributes.pm index 9808f9ee5..8fe81e53a 100644 --- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Attributes.pm +++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Attributes.pm @@ -615,6 +615,10 @@ sub attributes { 'default' => 300, 'type' => 'int' }, + 'bruteForceProtectionMaxFailed' => { + 'default' => 3, + 'type' => 'int' + }, 'bruteForceProtectionTempo' => { 'default' => 30, 'type' => 'int' diff --git a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm index f4431bbf8..40004fd15 100644 --- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm +++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm @@ -612,7 +612,13 @@ sub attributes { default => 300, type => 'int', documentation => - 'Brute force attack protection -> Max age third failed login', + 'Brute force attack protection -> Max age between last and first allowed failed login', + }, + bruteForceProtectionMaxFailed => { + default => 3, + type => 'int', + documentation => + 'Brute force attack protection -> Max allowed failed login', }, grantSessionRules => { type => 'grantContainer', diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/BruteForceProtection.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/BruteForceProtection.pm index 5152ba3be..0a243fc6c 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/BruteForceProtection.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/BruteForceProtection.pm @@ -27,7 +27,7 @@ sub init { sub run { my ( $self, $req ) = @_; - my $MaxAge = 0; + my $MaxAge = $self->conf->{bruteForceProtectionMaxAge} + 1; my $countFailed = 0; my @lastFailedLoginEpoch = (); @@ -37,32 +37,39 @@ sub run { } $self->logger->debug(" Number of failedLogin = $countFailed"); - return PE_OK if ( $countFailed < 3 ); + return PE_OK + if ( $countFailed <= $self->conf->{bruteForceProtectionMaxFailed} ); - foreach ( 0 .. 2 ) { - if ( defined $req->sessionInfo->{_loginHistory}->{failedLogin}->[$_] ) { + foreach ( 0 .. $self->conf->{bruteForceProtectionMaxFailed} - 1 ) { + if ( defined $req->sessionInfo->{_loginHistory}->{failedLogin}->[$_] ) + { push @lastFailedLoginEpoch, - $req->sessionInfo->{_loginHistory}->{failedLogin}->[$_]->{_utime}; + $req->sessionInfo->{_loginHistory}->{failedLogin}->[$_] + ->{_utime}; } } $self->logger->debug("BruteForceProtection enabled"); - # If Auth_N-2 older than MaxAge -> another try allowed - $MaxAge = $lastFailedLoginEpoch[0] - $lastFailedLoginEpoch[2]; + # If Auth_N-MaxFailed older than MaxAge -> another try allowed + $MaxAge + = $lastFailedLoginEpoch[0] + - $lastFailedLoginEpoch[ $self->conf->{bruteForceProtectionMaxFailed} - 1 ] + if $self->conf->{bruteForceProtectionMaxFailed}; $self->logger->debug(" -> MaxAge = $MaxAge"); return PE_OK - if ( $MaxAge > $self->conf->{bruteForceProtectionMaxAge} ); + if ( $MaxAge > $self->conf->{bruteForceProtectionMaxAge} ); # Delta between the two last failed logins -> Auth_N - Auth_N-1 - my $delta = time - $lastFailedLoginEpoch[1]; + my $delta = 0; + $delta = time - $lastFailedLoginEpoch[1] + if defined $lastFailedLoginEpoch[1]; $self->logger->debug(" -> Delta = $delta"); - # Delta between the two last failed logins < 30s => wait + # Delta between the two last failed logins < Tempo => wait return PE_OK - unless ( $delta <= $self->conf->{bruteForceProtectionTempo} ); + unless ( $delta <= $self->conf->{bruteForceProtectionTempo} ); # Account locked - #shift @{ $req->sessionInfo->{_loginHistory}->{failedLogin} }; return PE_WAIT; } diff --git a/lemonldap-ng-portal/t/61-BruteForceProtection.t b/lemonldap-ng-portal/t/61-BruteForceProtection.t index 8de0d8f4e..da46a1747 100644 --- a/lemonldap-ng-portal/t/61-BruteForceProtection.t +++ b/lemonldap-ng-portal/t/61-BruteForceProtection.t @@ -18,6 +18,9 @@ my $client = LLNG::Manager::Test->new( loginHistoryEnabled => 1, bruteForceProtection => 1, bruteForceProtectionTempo => 5, + bruteForceProtectionMaxFailed => 4, + failedLoginNumber => 6, + successLoginNumber => 4, } } ); @@ -30,7 +33,7 @@ ok( length => 23, accept => 'text/html', ), - 'Auth query' + '1st Auth query' ); count(1); my $id1 = expectCookie($res); @@ -46,7 +49,55 @@ ok( length => 23, accept => 'text/html', ), - 'Auth query' + '2nd Auth query' +); +count(1); +$id1 = expectCookie($res); +expectRedirection( $res, 'http://auth.example.com/' ); + +$client->logout($id1); + +## Third successful connection +ok( + $res = $client->_post( + '/', + IO::String->new('user=dwho&password=dwho'), + length => 23, + accept => 'text/html', + ), + '3rd Auth query' +); +count(1); +$id1 = expectCookie($res); +expectRedirection( $res, 'http://auth.example.com/' ); + +$client->logout($id1); + +## Forth successful connection +ok( + $res = $client->_post( + '/', + IO::String->new('user=dwho&password=dwho'), + length => 23, + accept => 'text/html', + ), + '4th Auth query' +); +count(1); +$id1 = expectCookie($res); +expectRedirection( $res, 'http://auth.example.com/' ); + +$client->logout($id1); + +## Fifth successful connection +ok( + $res = $client->_post( + '/', + IO::String->new('user=dwho&password=dwho'), + length => 23, + accept => 'text/html', + ), + '5th Auth query' ); count(1); $id1 = expectCookie($res); @@ -61,7 +112,7 @@ ok( IO::String->new('user=dwho&password=ohwd'), length => 23 ), - 'Auth query' + '1st Bad Auth query' ); count(1); expectReject($res); @@ -73,12 +124,36 @@ ok( IO::String->new('user=dwho&password=ohwd'), length => 23 ), - 'Auth query' + '2nd Bad Auth query' ); count(1); expectReject($res); -## Third failed connection -> rejected +## Third failed connection +ok( + $res = $client->_post( + '/', + IO::String->new('user=dwho&password=ohwd'), + length => 23 + ), + '3rd Bad Auth query' +); +count(1); +expectReject($res); + +## Forth failed connection +ok( + $res = $client->_post( + '/', + IO::String->new('user=dwho&password=ohwd'), + length => 23 + ), + '4th Bad Auth query' +); +count(1); +expectReject($res); + +## Fifth failed connection -> rejected ok( $res = $client->_post( '/', @@ -86,15 +161,15 @@ ok( length => 23, accept => 'text/html', ), - 'Auth query' + '5th Bad Auth query' ); count(1); -ok( $res->[2]->[0] =~ /<\/span>/, 'Protection enabled' ); +ok( $res->[2]->[0] =~ /<\/span>/, 'Rejected -> Protection enabled' ); count(1); sleep 1; -## Fourth failed connection -> Rejected +## Sixth failed connection -> Rejected ok( $res = $client->_post( '/', @@ -102,15 +177,15 @@ ok( length => 23, accept => 'text/html', ), - 'Auth query' + '6th Bad Auth query' ); count(1); -ok( $res->[2]->[0] =~ /<\/span>/, 'Protection enabled' ); +ok( $res->[2]->[0] =~ /<\/span>/, 'Rejected -> Protection enabled' ); count(1); sleep 2; -## Third successful connection -> Rejected +## Sixth successful connection -> Rejected ok( $res = $client->_post( '/', @@ -118,15 +193,15 @@ ok( length => 23, accept => 'text/html', ), - 'Auth query' + '6th Auth query' ); count(1); -ok( $res->[2]->[0] =~ /<\/span>/, 'Protection enabled' ); +ok( $res->[2]->[0] =~ /<\/span>/, 'Rejected -> Protection enabled' ); count(1); sleep 3; -## Fourth successful connection -> Accepted +## Seventh successful connection -> Accepted ok( $res = $client->_post( '/', @@ -134,7 +209,7 @@ ok( length => 37, accept => 'text/html', ), - 'Auth query' + '7th Auth query' ); count(1); $id1 = expectCookie($res); @@ -145,9 +220,9 @@ ok( $res->[2]->[0] =~ /trspan="lastLogins"/, 'History found' ) my @c = ( $res->[2]->[0] =~ /127.0.0.1/gs ); my @cf = ( $res->[2]->[0] =~ /PE5<\/td>/gs ); -# History with 5 entries -ok( @c == 7, ' -> Seven entries found' ); -ok( @cf == 4, " -> Four 'failedLogin' entries found" ); +# History with 10 entries +ok( @c == 10, ' -> Ten entries found' ); +ok( @cf == 6, " -> Six 'failedLogin' entries found" ); count(3); $client->logout($id1);