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