Bug#1033653: bullseye-pu: package lemonldap-ng/2.0.11+ds-4+deb11u
Yadd
yadd at debian.org
Wed Mar 29 13:26:30 BST 2023
Package: release.debian.org
Severity: normal
Tags: bullseye
User: release.debian.org at packages.debian.org
Usertags: pu
X-Debbugs-Cc: lemonldap-ng at packages.debian.org, security at 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
-------------- next part --------------
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 <yadd at 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 <yadd at 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 <yadd at 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
More information about the pkg-perl-maintainers
mailing list