Version in base suite: 2.0.11+ds-4+deb11u2 Base version: lemonldap-ng_2.0.11+ds-4+deb11u2 Target version: lemonldap-ng_2.0.11+ds-4+deb11u4 Base file: /srv/ftp-master.debian.org/ftp/pool/main/l/lemonldap-ng/lemonldap-ng_2.0.11+ds-4+deb11u2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/l/lemonldap-ng/lemonldap-ng_2.0.11+ds-4+deb11u4.dsc NEWS | 12 changelog | 13 + patches/CVE-2023-28862.patch | 401 ++++++++++++++++++++++++++++++++ patches/fix-url-validation-bypass.patch | 66 +++++ patches/series | 2 5 files changed, 494 insertions(+) diff -Nru lemonldap-ng-2.0.11+ds/debian/NEWS lemonldap-ng-2.0.11+ds/debian/NEWS --- lemonldap-ng-2.0.11+ds/debian/NEWS 2022-09-11 07:28:30.000000000 +0000 +++ lemonldap-ng-2.0.11+ds/debian/NEWS 2023-03-29 11:50:40.000000000 +0000 @@ -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 Wed, 29 Mar 2023 15:24:20 +0400 + lemonldap-ng (2.0.9+ds-1) unstable; urgency=medium CVE-2020-24660 diff -Nru lemonldap-ng-2.0.11+ds/debian/changelog lemonldap-ng-2.0.11+ds/debian/changelog --- lemonldap-ng-2.0.11+ds/debian/changelog 2022-09-11 07:28:30.000000000 +0000 +++ lemonldap-ng-2.0.11+ds/debian/changelog 2023-03-29 11:50:40.000000000 +0000 @@ -1,3 +1,16 @@ +lemonldap-ng (2.0.11+ds-4+deb11u4) bullseye; urgency=medium + + * Fix 2FA issue when using AuthBasic handler (CVE-2023-28862) + + -- Yadd Wed, 29 Mar 2023 15:50:40 +0400 + +lemonldap-ng (2.0.11+ds-4+deb11u3) bullseye; urgency=medium + + * Fix URL validation bypass + (https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/issues/2832) + + -- Yadd Sun, 05 Feb 2023 18:03:16 +0400 + lemonldap-ng (2.0.11+ds-4+deb11u2) bullseye; urgency=medium * Add patch to improve session destroy propagation (Closes: CVE-2022-37186) diff -Nru lemonldap-ng-2.0.11+ds/debian/patches/CVE-2023-28862.patch lemonldap-ng-2.0.11+ds/debian/patches/CVE-2023-28862.patch --- lemonldap-ng-2.0.11+ds/debian/patches/CVE-2023-28862.patch 1970-01-01 00:00:00.000000000 +0000 +++ lemonldap-ng-2.0.11+ds/debian/patches/CVE-2023-28862.patch 2023-03-29 11:50:40.000000000 +0000 @@ -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 +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 `. ++ ++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 -Nru lemonldap-ng-2.0.11+ds/debian/patches/fix-url-validation-bypass.patch lemonldap-ng-2.0.11+ds/debian/patches/fix-url-validation-bypass.patch --- lemonldap-ng-2.0.11+ds/debian/patches/fix-url-validation-bypass.patch 1970-01-01 00:00:00.000000000 +0000 +++ lemonldap-ng-2.0.11+ds/debian/patches/fix-url-validation-bypass.patch 2023-03-29 11:50:40.000000000 +0000 @@ -0,0 +1,66 @@ +Description: Fix URL validation bypass + An attacker can forge a redirection on a malicious site using a fake credentials in URL value. + . + Example: + . + Portal : https://auth.openid.club + Allowed application : https://test1.openid.club + Malicious site : https://google.fr + Malicious URL : https://test1.openid.club:test@google.fr + Malicious URL base 64 : aHR0cHM6Ly90ZXN0MS5vcGVuaWQuY2x1Yjp0ZXN0QGdvb2dsZS5mcgo= + Malicious redirection trigger : https://auth.openid.club/?url=aHR0cHM6Ly90ZXN0MS5vcGVuaWQuY2x1Yjp0ZXN0QGdvb2dsZS5mcgo= +Author: Maxime Besson +Origin: upstream, commit:88d3507d commit:e6156db0 +Bug: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/issues/2832 +Forwarded: not-needed +Applied-Upstream: 2.0.16 +Reviewed-By: Yadd +Last-Update: 2023-02-05 + +--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm ++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm +@@ -8,6 +8,7 @@ + use MIME::Base64; + use POSIX qw(strftime); + use Lemonldap::NG::Portal::Main::Constants qw(portalConsts); ++use URI; + + # Main method + # ----------- +@@ -152,12 +153,19 @@ + } + + # Unprotected hosts +- my ( $proto, $vhost, $appuri ) = $tmp =~ m#^(https?://)([^/]*)(.*)#; +- $vhost =~ s/:\d+$//; ++ my ( $proto, $vhost, $appuri ); ++ if ($tmp) { ++ my $u = URI->new($tmp); ++ if ( $u->scheme =~ /^https?$/ ) { ++ $proto = $u->scheme; ++ $vhost = $u->host if $u->can("host"); ++ $appuri = $u->path_query; ++ } ++ } + + # Try to resolve alias + my $originalVhost = $self->HANDLER->resolveAlias($vhost); +- $vhost = $proto . $originalVhost; ++ $vhost = $proto . '://' . $originalVhost; + $self->logger->debug( "Required URL (param: " + . ( $req->param('logout') ? 'HTTP Referer' : 'urldc' ) + . " | value: $tmp | alias: $vhost)" ); +--- a/lemonldap-ng-portal/t/03-XSS-protection.t ++++ b/lemonldap-ng-portal/t/03-XSS-protection.t +@@ -80,6 +80,11 @@ + => 0, + 'base64 encoded HTML tags', + ++ # Make sure userinfo does not confuse URL parsing (#2832) ++ # https://test1.example.com:test@hacker.com ++ 'aHR0cHM6Ly90ZXN0MS5leGFtcGxlLmNvbTp0ZXN0QGhhY2tlci5jb20=' => 0, ++ 'userinfo trick', ++ + # LOGOUT TESTS + 'LOGOUT', + diff -Nru lemonldap-ng-2.0.11+ds/debian/patches/series lemonldap-ng-2.0.11+ds/debian/patches/series --- lemonldap-ng-2.0.11+ds/debian/patches/series 2022-09-11 07:28:30.000000000 +0000 +++ lemonldap-ng-2.0.11+ds/debian/patches/series 2023-03-29 11:50:40.000000000 +0000 @@ -10,3 +10,5 @@ dont-display-totp-secret.patch CVE-2021-40874.patch CVE-2022-37186.patch +fix-url-validation-bypass.patch +CVE-2023-28862.patch