[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1033653: marked as done (bullseye-pu: package lemonldap-ng/2.0.11+ds-4+deb11u4)



Your message dated Sat, 29 Apr 2023 10:54:14 +0100
with message-id <502b8fb37ece620c9723446611a9287974ba5a0c.camel@adam-barratt.org.uk>
and subject line Closing p-u requests for fixes included in 11.7
has caused the Debian Bug report #1030598,
regarding bullseye-pu: package lemonldap-ng/2.0.11+ds-4+deb11u4
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact owner@bugs.debian.org
immediately.)


-- 
1030598: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1030598
Debian Bug Tracking System
Contact owner@bugs.debian.org with problems
--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: bullseye
User: release.debian.org@packages.debian.org
Usertags: pu
X-Debbugs-Cc: lemonldap-ng@packages.debian.org, security@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 <yadd@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@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@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

--- End Message ---
--- Begin Message ---
Package: release.debian.org
Version: 11.7

Hi,

Each of the updates referred to in these requests was included in this
morning's 11.7 point release.

Regards,

Adam

--- End Message ---

Reply to: