Package: release.debian.org Severity: normal Tags: bullseye User: release.debian....@packages.debian.org Usertags: pu X-Debbugs-Cc: lemonldap...@packages.debian.org, secur...@debian.org Control: affects -1 + src:lemonldap-ng
[ Reason ] lemonldap-ng is vulnarable to a second factor bypass when used with an "AuthBasic handler" (generally used for non-browser apps). [ Impact ] Medium security issue. [ Tests ] New test proves that issue is fixed [ Risks ] Low risk, patch isn't so big and test coverage looks good [ Checklist ] [X] *all* changes are documented in the d/changelog [X] I reviewed all changes and I approve them [X] attach debdiff against the package in (old)stable [X] the issue is verified as fixed in unstable [ Changes ] No more allow to accept basic authentication in AuthBasic handler when a second factor is required, add also an environment variable to restore previous behavior. [ Other info ] I didn't pushed yet the already accepted patch for deb11u3 (#1030598). Maybe we could join and push directly deb11u4 into Bullseye. Cheers, Yadd
diff --git a/debian/NEWS b/debian/NEWS index b8955920b..c4d7ee951 100644 --- a/debian/NEWS +++ b/debian/NEWS @@ -1,3 +1,15 @@ +lemonldap-ng (2.0.11+ds-4+deb11u4) bullseye; urgency=medium + + AuthBasic now enforces 2FA activation (CVE-2023-28862): + In previous versions of LemonLDAP::NG, a 2FA protected account didn't need + to use their second factor when authenticating to an AuthBasic handler. + If you want 2FA protected accounts to access AuthBasic handlers, which are + password only, you can add the following test in your 2FA activation rules: + + and not $ENV{AuthBasic} + + -- Yadd <y...@debian.org> Wed, 29 Mar 2023 15:24:20 +0400 + lemonldap-ng (2.0.9+ds-1) unstable; urgency=medium CVE-2020-24660 diff --git a/debian/changelog b/debian/changelog index b6f666f69..5d2c62ac0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +lemonldap-ng (2.0.11+ds-4+deb11u4) bullseye; urgency=medium + + * Fix 2FA issue when using AuthBasic handler (CVE-2023-28862) + + -- Yadd <y...@debian.org> Wed, 29 Mar 2023 15:50:40 +0400 + lemonldap-ng (2.0.11+ds-4+deb11u3) bullseye; urgency=medium * Fix URL validation bypass diff --git a/debian/patches/CVE-2023-28862.patch b/debian/patches/CVE-2023-28862.patch new file mode 100644 index 000000000..9fb5d9d23 --- /dev/null +++ b/debian/patches/CVE-2023-28862.patch @@ -0,0 +1,401 @@ +Description: fix AuthBasic security issue when used with second factor + To simplify, AuthBasic accepted connections even if 2FA failed +Author: Yadd <y...@debian.org> +Bug: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/issues/2896 +Forwarded: not-needed +Applied-Upstream: 2.16.1, (https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/merge_requests/334) +Last-Update: 2023-03-29 + +--- a/doc/sources/admin/upgrade_2_0_x.rst ++++ b/doc/sources/admin/upgrade_2_0_x.rst +@@ -26,6 +26,19 @@ + + None + ++2.16.1 ++-------- ++ ++AuthBasic now enforces 2FA activation ++~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ++ ++In previous versions of LemonLDAP::NG, a 2FA protected account didn't need to use their second factor when authenticating to an :doc:`AuthBasic handler <authbasichandler>`. ++ ++If you are *absolutely sure* that you want 2FA protected accounts to access AuthBasic handlers, which are password only, you can add the following test in your 2FA activation rules :: +++ +++ and not $ENV{AuthBasic} +++ +++ + 2.0.11 + ------ + +--- a/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/AuthBasic.pm ++++ b/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/AuthBasic.pm +@@ -28,9 +28,8 @@ + my ( $class, $req ) = @_; + if ( my $creds = $req->env->{'HTTP_AUTHORIZATION'} ) { + $creds =~ s/^Basic\s+//; +- my @date = localtime; +- my $day = $date[5] * 366 + $date[7]; +- return Digest::SHA::sha256_hex( $creds . $day ); ++ my $pepper = int( time / $class->tsv->{timeout} ) . $class->tsv->{keyH}; ++ return sha256_hex( $creds . $pepper ); + } + else { + return 0; +--- a/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Reload.pm ++++ b/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Reload.pm +@@ -5,6 +5,7 @@ + package Lemonldap::NG::Handler::Main; + + use strict; ++use Digest::SHA qw(sha256_hex); + use Lemonldap::NG::Common::Conf::Constants; #inherits + use Lemonldap::NG::Common::Crypto; + use Lemonldap::NG::Common::Safelib; #link protected safe Safe object +@@ -208,6 +209,7 @@ + ); + + $class->tsv->{cipher} = Lemonldap::NG::Common::Crypto->new( $conf->{key} ); ++ $class->tsv->{keyH} = sha256_hex( $conf->{key} ); + + foreach my $opt (qw(https port maintenance)) { + +--- a/lemonldap-ng-portal/MANIFEST ++++ b/lemonldap-ng-portal/MANIFEST +@@ -579,6 +579,7 @@ + t/35-My-session.t + t/35-REST-config-backend.t + t/35-REST-export-password.t ++t/35-REST-sessions-with-AuthBasic-handler-with-2FA.t + t/35-REST-sessions-with-AuthBasic-handler.t + t/35-REST-sessions-with-REST-server.t + t/35-SOAP-config-backend.t +--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm ++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm +@@ -488,8 +488,6 @@ + # $user passed by BruteForceProtection plugin + my ( $self, $req, $user ) = @_; + +- # Do not restore infos if session already opened +- unless ( $req->id ) { + my $key = $req->{sessionInfo}->{ $self->conf->{whatToTrace} } || $user; + return PE_OK unless ( $key and length($key) ); + +@@ -505,7 +503,6 @@ + $req->{sessionInfo}->{$k} = $persistentSession->data->{$k}; + } + } +- } + PE_OK; + } + +--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/RESTServer.pm ++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/RESTServer.pm +@@ -293,7 +293,7 @@ + unless ($session); + + $self->logger->debug( +- "SOAP request create a new session (" . $session->id . ")" ); ++ "REST request create a new session (" . $session->id . ")" ); + + return $self->p->sendJSONresponse( $req, + { result => 1, session => $session->data } ); +@@ -308,13 +308,14 @@ + return $self->p->sendError( $req, 'Bad secret', 403 ); + } + ++ $req->env->{AuthBasic} = 1; + $req->{id} = $id; + $req->{force} = 1; + $req->user( $req->param('user') ); + $req->data->{password} = $req->param('password'); + $req->steps( [ + @{ $self->p->beforeAuth }, +- qw(getUser extractFormInfo authenticate setAuthSessionInfo), ++ $self->p->authProcess, + @{ $self->p->betweenAuthAndData }, + $self->p->sessionData, + @{ $self->p->afterData }, +@@ -326,7 +327,8 @@ + $self->logger->debug( + "REST authentication result for $req->{user}: code $req->{error}"); + +- if ( $req->error > 0 ) { ++ if ( $req->error != 0 ) { ++ $self->p->deleteSession($req); + return $self->p->sendError( $req, 'Bad credentials', 401 ); + } + return $self->session( $req, $id ); +--- /dev/null ++++ b/lemonldap-ng-portal/t/35-REST-sessions-with-AuthBasic-handler-with-2FA.t +@@ -0,0 +1,270 @@ ++use warnings; ++use lib 'inc'; ++use strict; ++use File::Temp 'tempdir'; ++use IO::String; ++use JSON; ++use MIME::Base64; ++use Test::More; ++ ++no warnings 'once'; ++ ++our $debug = 'error'; ++our $maintests = 51; ++my ( $p, $res, $spId ); ++$| = 1; ++ ++$LLNG::TMPDIR = tempdir( 'tmpSessionXXXXX', DIR => 't/sessions', CLEANUP => 1 ); ++ ++require 't/separate-handler.pm'; ++ ++require "t/test-lib.pm"; ++ ++SKIP: { ++ eval { require Convert::Base32 }; ++ if ($@) { ++ skip 'Convert::Base32 is missing', $maintests; ++ } ++ eval { require Authen::OATH }; ++ if ($@) { ++ skip 'Authen::OATH is missing', $maintests; ++ } ++ ++ ok( $p = issuer(), 'Issuer portal' ); ++ ++ # BEGIN TESTS ++ ok( $res = handler( req => [ GET => 'http://test2.example.com/' ] ), ++ 'Simple request to handler' ); ++ ok( ++ getHeader( $res, 'WWW-Authenticate' ) eq 'Basic realm="LemonLDAP::NG"', ++ 'Get WWW-Authenticate header' ++ ); ++ ++ my $subtest = 0; ++ foreach my $user (qw(dwho)) { ++ ok( $res = $p->_get( '/', accept => 'text/html' ), 'Get Menu', ); ++ my ( $host, $url, $query ) = ++ expectForm( $res, '#', undef, 'user', 'password' ); ++ ++ $query =~ s/user=/user=dwho/; ++ $query =~ s/password=/password=dwho/; ++ ok( ++ $res = $p->_post( ++ '/', ++ IO::String->new($query), ++ length => length($query), ++ accept => 'text/html', ++ ), ++ 'Auth query' ++ ); ++ my $id = expectCookie($res); ++ expectRedirection( $res, 'http://auth.idp.com' ); ++ ++ # TOTP form ++ ok( ++ $res = $p->_get( ++ '/2fregisters', ++ cookie => "lemonldap=$id", ++ accept => 'text/html', ++ ), ++ 'Form registration' ++ ); ++ expectRedirection( $res, qr#/2fregisters/totp$# ); ++ ok( ++ $res = $p->_get( ++ '/2fregisters/totp', ++ cookie => "lemonldap=$id", ++ accept => 'text/html', ++ ), ++ 'Form registration' ++ ); ++ ok( $res->[2]->[0] =~ /totpregistration\.(?:min\.)?js/, ++ 'Found TOTP js' ); ++ ++ # JS query ++ ok( ++ $res = $p->_post( ++ '/2fregisters/totp/getkey', IO::String->new(''), ++ cookie => "lemonldap=$id", ++ length => 0, ++ ), ++ 'Get new key' ++ ); ++ eval { $res = JSON::from_json( $res->[2]->[0] ) }; ++ ok( not($@), 'Content is JSON' ) ++ or explain( $res->[2]->[0], 'JSON content' ); ++ my ( $key, $token ); ++ ok( $key = $res->{secret}, 'Found secret' ); ++ ok( $token = $res->{token}, 'Found token' ); ++ $key = Convert::Base32::decode_base32($key); ++ ++ # Post code ++ my $code; ++ ok( $code = Lemonldap::NG::Common::TOTP::_code( undef, $key, 0, 30, 6 ), ++ 'Code' ); ++ ok( $code =~ /^\d{6}$/, 'Code contains 6 digits' ); ++ ++ my $s = "code=$code&token=$token"; ++ ok( ++ $res = $p->_post( ++ '/2fregisters/totp/verify', ++ IO::String->new($s), ++ length => length($s), ++ cookie => "lemonldap=$id", ++ ), ++ 'Post code' ++ ); ++ eval { $res = JSON::from_json( $res->[2]->[0] ) }; ++ ok( not($@), 'Content is JSON' ) ++ or explain( $res->[2]->[0], 'JSON content' ); ++ ok( $res->{result} == 1, 'Key is registered' ); ++ ok( $res = $p->_get( '/', accept => 'text/html' ), 'Get Menu', ); ++ ( $host, $url, $query ) = ++ expectForm( $res, '#', undef, 'user', 'password' ); ++ ++ $query =~ s/user=/user=dwho/; ++ $query =~ s/password=/password=dwho/; ++ ok( ++ $res = $p->_post( ++ '/', ++ IO::String->new($query), ++ length => length($query), ++ accept => 'text/html', ++ ), ++ 'Auth query' ++ ); ++ ( $host, $url, $query ) = expectForm( $res, undef, '/totp2fcheck' ); ++ ++ ok( ++ $res = handler( ++ req => [ ++ GET => 'http://test2.example.com/', ++ [ ++ 'Authorization' => 'Basic ' ++ . encode_base64( "$user:$user", '' ) ++ ] ++ ], ++ sub => sub { ++ my ($res) = @_; ++ $subtest++; ++ subtest 'REST request to Portal' => sub { ++ plan tests => 2; ++ ok( $res->[0] eq 'POST', 'Get POST request' ); ++ my ( $url, $query ) = split /\?/, $res->[1]; ++ ok( ++ $res = $p->_post( ++ $url, IO::String->new( $res->[3] ), ++ length => length( $res->[3] ), ++ query => $query, ++ ), ++ 'Push request to portal' ++ ); ++ return $res; ++ }; ++ return $res; ++ }, ++ ), ++ 'AuthBasic request' ++ ); ++ ok( $res->[0] == 401, "Authentication rejected"); ++ } ++ ok( $subtest == 1, 'REST requests were done by handler' ); ++ ++ ++ $subtest=0; ++ foreach my $user (qw(dwho)) { ++ ok( ++ $res = handler( ++ req => [ ++ GET => 'http://test2.example.com/', ++ [ ++ 'Authorization' => 'Basic ' ++ . encode_base64( "$user:$user", '' ) ++ ] ++ ], ++ sub => sub { ++ my ($res) = @_; ++ $subtest++; ++ subtest 'REST request to Portal' => sub { ++ plan tests => 2; ++ ok( $res->[0] eq 'POST', 'Get POST request' ); ++ my ( $url, $query ) = split /\?/, $res->[1]; ++ ok( ++ $res = $p->_post( ++ $url, IO::String->new( $res->[3] ), ++ length => length( $res->[3] ), ++ query => $query, ++ ), ++ 'Push request to portal' ++ ); ++ return $res; ++ }; ++ return $res; ++ }, ++ ), ++ 'New AuthBasic request' ++ ); ++ ok( $subtest == 1, 'Handler used its local cache' ); ++ ok( $res->[0] == 401, 'Authentication rejected a second time'); ++ } ++ ++ foreach my $user (qw(rtyler)) { ++ ok( ++ $res = handler( ++ req => [ ++ GET => 'http://test2.example.com/', ++ [ ++ 'Authorization' => 'Basic ' ++ . encode_base64( "$user:$user", '' ) ++ ] ++ ], ++ sub => sub { ++ my ($res) = @_; ++ $subtest++; ++ subtest 'REST request to Portal' => sub { ++ plan tests => 2; ++ ok( $res->[0] eq 'POST', 'Get POST request' ); ++ my ( $url, $query ) = split /\?/, $res->[1]; ++ ok( ++ $res = $p->_post( ++ $url, IO::String->new( $res->[3] ), ++ length => length( $res->[3] ), ++ query => $query, ++ ), ++ 'Push request to portal' ++ ); ++ return $res; ++ }; ++ return $res; ++ }, ++ ), ++ 'New AuthBasic request' ++ ); ++ ok( $subtest == 2, 'Portal was called a second time' ); ++ is( $res->[0], 200, ++ '2FA did not trigger for rtyler because of ENV rule' ); ++ } ++ ++ end_handler(); ++ clean_sessions(); ++} ++done_testing(); ++ ++sub issuer { ++ return LLNG::Manager::Test->new( { ++ ini => { ++ logLevel => $debug, ++ domain => 'idp.com', ++ portal => 'http://auth.idp.com', ++ authentication => 'Demo', ++ userDB => 'Same', ++ restSessionServer => 1, ++ totp2fActivation => ++ 'has2f("TOTP") and ($uid eq "dwho" or not $ENV{AuthBasic})', ++ totp2fSelfRegistration => 1, ++ totp2fRange => 2, ++ totp2fAuthnLevel => 5, ++ } ++ } ++ ); ++} diff --git a/debian/patches/series b/debian/patches/series index 8b9338fec..8cd6b510b 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -11,3 +11,4 @@ dont-display-totp-secret.patch CVE-2021-40874.patch CVE-2022-37186.patch fix-url-validation-bypass.patch +CVE-2023-28862.patch