Package: release.debian.org Severity: normal User: release.debian....@packages.debian.org Usertags: unblock
Please unblock package lemonldap-ng Hi all, during an internal audit, one of lemonldap-ng's developers discovered an attack vector (#928944, CVE-2019-12046). It opens 3 security issues: - [high] for 2.0.0 ≤ version < 2.0.4: when CSRF tokens are enabled (default) and tokens are stored in session DB (not default, used with poor load-balancers), the token can be used to open an anonymous short-life session (2mn). It allows one to access to all aplications without additional rules - [medium] for every versions < 2.0.4 or 1.9.19 when SAML/OIDC tokens are stored in sessions DB (not default), tokens can be used to have an anonymous session - [low] for every versions < 2.0.4 or 1.9.19: when self-registration is allowed, mail token can be used to have an anonymous session. The patch contains 3 parts: - the fix itself: * lemonldap-ng-common/lib/Lemonldap/NG/Common/Session.pm * lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OneTimeToken.pm * lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/MailPasswordReset.pm * lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SMTP.pm - regressions workaround: * REST, SOAP, CDA and "Main::Run" files * lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Remote.pm - 3 upstream tests to prove that issues are fixed * lemonldap-ng-portal/t/41-Token-Global-Storage.t * lemonldap-ng-portal/t/42-Register-Security.t * lemonldap-ng-portal/t/77-2F-Mail-with-global-storage.t lemonldap-ng has no reverse dependencies. Upstream provides more than 9000 unit tests that runs all main features, so I think it low risky to unblock lemonldap-ng. Cheers, Xavier unblock lemonldap-ng/2.0.2+ds-7+deb10u1 -- System Information: Debian Release: buster/sid APT prefers testing APT policy: (900, 'testing'), (500, 'unstable') Architecture: amd64 (x86_64) Kernel: Linux 4.19.0-4-amd64 (SMP w/8 CPU cores) Kernel taint flags: TAINT_OOT_MODULE, TAINT_UNSIGNED_MODULE Locale: LANG=fr_FR.UTF-8, LC_CTYPE=fr_FR.UTF-8 (charmap=UTF-8), LANGUAGE= (charmap=UTF-8) Shell: /bin/sh linked to /usr/bin/dash Init: systemd (via /run/systemd/system) LSM: AppArmor: enabled
diff --git a/debian/changelog b/debian/changelog index 9bf7afa99..216a6aa65 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +lemonldap-ng (2.0.2+ds-7+deb10u1) unstable; urgency=high + + * Fix tokens security (Closes: #928944, CVE-2019-12046) + + -- Xavier Guimard <y...@debian.org> Mon, 13 May 2019 21:22:34 +0200 + lemonldap-ng (2.0.2+ds-7) unstable; urgency=medium * Import upstream translations update diff --git a/debian/patches/CVE-2019-12046.patch b/debian/patches/CVE-2019-12046.patch new file mode 100644 index 000000000..a1d144478 --- /dev/null +++ b/debian/patches/CVE-2019-12046.patch @@ -0,0 +1,530 @@ +Description: Fix for CVE XXXX + When CSRF is enabled (default) and tokens are stored in session database + (not default, used for poor load balancers), a short-life session can be + created without being authentified. + This patch fixes also a low level vulnerability on self-register (same vector, + see https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues/1743) + . + This patch adds also 2 new upstream tests to prove that issues are fixed. + . + https://security-tracker.debian.org/tracker/CVE-2019-12046 +Author: Xavier Guimard <y...@debian.org> +Origin: upstream, https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues/1742 +Bug: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues/1742 +Bug-Debian: https://bugs.debian.org/928944 +Forwarded: not-needed +Last-Update: 2019-05-12 + +--- a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Apache/Session/REST.pm ++++ b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Apache/Session/REST.pm +@@ -21,7 +21,7 @@ + modified => 0, + }; + foreach ( +- qw(baseUrl user password realm localStorage localStorageOptions lwpOpts lwpSslOpts) ++ qw(baseUrl user password realm localStorage localStorageOptions lwpOpts lwpSslOpts kind) + ) + { + $self->{$_} = $args->{$_}; +@@ -116,8 +116,13 @@ + + sub getJson { + my $self = shift; +- my $url = shift; +- my $resp = $self->ua->get( $self->base . $url, @_ ); ++ my $id = shift; ++ my $resp = $self->ua->get( ++ $self->base ++ . $id ++ . ( $self->{kind} ne 'SSO' ? "?kind=$self->{kind}" : '' ), ++ @_ ++ ); + if ( $resp->is_success ) { + my $res; + eval { $res = from_json( $resp->content, { allow_nonref => 1 } ) }; +--- a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session.pm ++++ b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session.pm +@@ -139,6 +139,14 @@ + + # Load session data into object + if ($data) { ++ if ( $self->kind and $data->{_session_kind} ) { ++ unless ( $data->{_session_kind} eq $self->kind ) { ++ $self->error( ++ "Session kind mismatch : $data->{_session_kind} is not " ++ . $self->kind ); ++ return undef; ++ } ++ } + $self->_save_data($data); + $self->kind( $data->{_session_kind} ); + $self->id( $data->{_session_id} ); +@@ -158,7 +166,7 @@ + if ( $self->storageModule =~ /^Lemonldap::NG::Common::Apache::Session/ ) + { + tie %h, $self->storageModule, $self->id, +- { %{ $self->options }, %$options }; ++ { %{ $self->options }, %$options, kind => $self->kind }; + } + else { + tie %h, 'Lemonldap::NG::Common::Apache::Session', $self->id, +--- a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session/REST.pm ++++ b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session/REST.pm +@@ -248,7 +248,7 @@ + Lemonldap::NG::Handler::PSGI::Main->tsv->{sessionCacheOptions}, + id => $id, + force => $force, +- kind => $mod->{kind}, ++ ( $id ? () : ( kind => $mod->{kind} ) ), + ( $info ? ( info => $info ) : () ), + } + ); +@@ -271,6 +271,9 @@ + $self->error('Unknown (or unconfigured) session type'); + return (); + } ++ if ( my $kind = $req->params('kind') ) { ++ $m->{kind} = $kind; ++ } + return $m; + } + +--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OneTimeToken.pm ++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OneTimeToken.pm +@@ -5,7 +5,7 @@ + use JSON qw(from_json to_json); + use Crypt::URandom; + +-our $VERSION = '2.0.2'; ++our $VERSION = '2.0.4'; + + extends 'Lemonldap::NG::Common::Module'; + +@@ -76,7 +76,8 @@ + else { + + # Create a new session +- my $tsession = $self->p->getApacheSession( undef, info => $infos ); ++ my $tsession = ++ $self->p->getApacheSession( undef, info => $infos, kind => 'TOKEN' ); + $self->logger->debug("Token $tsession->{id} created"); + return $tsession->id; + } +@@ -108,7 +109,7 @@ + else { + + # Get token session +- my $tsession = $self->p->getApacheSession($id); ++ my $tsession = $self->p->getApacheSession( $id, kind => 'TOKEN' ); + unless ($tsession) { + $self->logger->notice("Bad (or expired) token $id"); + return undef; +@@ -133,7 +134,11 @@ + return $id; + } + else { +- $self->p->getApacheSession( $id, $k => $v ); ++ $self->p->getApacheSession( ++ $id, ++ kind => "TOKEN", ++ info => { $k => $v } ++ ); + return $id; + } + } +--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Remote.pm ++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Remote.pm +@@ -50,7 +50,7 @@ + cacheModule => $self->conf->{localSessionStorage}, + cacheModuleOptions => $self->conf->{localSessionStorageOptions}, + id => $rId, +- kind => "REMOTE", ++ kind => "SSO", + } + ); + +--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SMTP.pm ++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SMTP.pm +@@ -232,7 +232,7 @@ + + # Browse found sessions to check if it's a mail session + foreach my $id ( keys %$sessions ) { +- my $mailSession = $self->p->getApacheSession($id); ++ my $mailSession = $self->p->getApacheSession($id, kind => 'TOKEN'); + next unless ($mailSession); + return $mailSession if ( $mailSession->data->{_type} =~ /^mail$/ ); + } +@@ -257,7 +257,7 @@ + + # Browse found sessions to check if it's a register session + foreach my $id ( keys %$sessions ) { +- my $registerSession = $self->p->getApacheSession($id); ++ my $registerSession = $self->p->getApacheSession($id, kind => 'TOKEN'); + next unless ($registerSession); + return $id + if ( $registerSession->data->{_type} +--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm ++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm +@@ -318,7 +318,7 @@ + # If $id is set to undef or if $args{force} is true, return a new session. + sub getApacheSession { + my ( $self, $id, %args ) = @_; +- $args{kind} ||= "SSO"; ++ $args{kind} //= "SSO"; + if ($id) { + $self->logger->debug("Try to get $args{kind} session $id"); + } +--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CDA.pm ++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CDA.pm +@@ -31,33 +31,34 @@ + $self->logger->debug('CDA request'); + + # Create CDA session +- if ( my $cdaSession = +- $self->p->getApacheSession( undef, kind => "CDA" ) ) +- { +- my $cdaInfos = { '_utime' => time }; +- if ( $self->{conf}->{securedCookie} < 2 or $ssl ) { +- $cdaInfos->{cookie_value} = $req->id; +- $cdaInfos->{cookie_name} = $self->{conf}->{cookieName}; +- } +- else { +- $cdaInfos->{cookie_value} = +- $req->{sessionInfo}->{_httpSession}; +- $cdaInfos->{cookie_name} = $self->{conf}->{cookieName} . "http"; +- } +- +- $self->p->updateSession( $req, $cdaInfos, $cdaSession->id ); +- +- $req->{urldc} .= +- ( $urldc =~ /\?/ ? '&' : '?' ) +- . $self->{conf}->{cookieName} . "cda=" +- . $cdaSession->id; +- +- $self->logger->debug( "CDA redirection to " . $req->{urldc} ); ++ my $cdaInfos = { '_utime' => time }; ++ if ( $self->{conf}->{securedCookie} < 2 or $ssl ) { ++ $cdaInfos->{cookie_value} = $req->id; ++ $cdaInfos->{cookie_name} = $self->{conf}->{cookieName}; + } + else { ++ $cdaInfos->{cookie_value} = ++ $req->{sessionInfo}->{_httpSession}; ++ $cdaInfos->{cookie_name} = $self->{conf}->{cookieName} . "http"; ++ } ++ ++ my $cdaSession = ++ $self->p->getApacheSession( undef, kind => "CDA", info => $cdaInfos ); ++ unless ($cdaSession) { + $self->logger->error("Unable to create CDA session"); + return PE_APACHESESSIONERROR; + } ++ ++ # We are about to redirect the user to the CDA application, ++ # dismiss any previously stored redirections (#1650) ++ delete $req->{pdata}->{_url}; ++ ++ $req->{urldc} .= ++ ( $urldc =~ /\?/ ? '&' : '?' ) ++ . $self->{conf}->{cookieName} . "cda=" ++ . $cdaSession->id; ++ ++ $self->logger->debug( "CDA redirection to " . $req->{urldc} ); + } + PE_OK; + } +--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/MailPasswordReset.pm ++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/MailPasswordReset.pm +@@ -112,7 +112,7 @@ + $self->logger->debug( "Token given for password reset: " . $mailToken ); + + # Check if token is valid +- my $mailSession = $self->p->getApacheSession($mailToken); ++ my $mailSession = $self->p->getApacheSession($mailToken, kind => 'TOKEN'); + unless ($mailSession) { + $self->userLogger->warn('Bad reset token'); + return PE_BADMAILTOKEN; +@@ -251,7 +251,7 @@ + $infos->{_pdata} = $req->pdata; + + # create session +- $mailSession = $self->p->getApacheSession( undef, info => $infos ); ++ $mailSession = $self->p->getApacheSession( undef, kind => 'TOKEN', info => $infos ); + + $req->id( $mailSession->id ); + } +--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/SOAPServer.pm ++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/SOAPServer.pm +@@ -272,7 +272,7 @@ + my ( $self, $req, $id ) = @_; + die 'id is required' unless ($id); + +- my $session = $self->p->getApacheSession($id); ++ my $session = $self->p->getApacheSession( $id, kind => '' ); + + my @tmp = (); + unless ($session) { +--- /dev/null ++++ b/lemonldap-ng-portal/t/41-Token-Global-Storage.t +@@ -0,0 +1,84 @@ ++use Test::More; ++use strict; ++use IO::String; ++ ++require 't/test-lib.pm'; ++ ++my $res; ++ ++my $client = LLNG::Manager::Test->new( { ++ ini => { ++ logLevel => 'error', ++ useSafeJail => 1, ++ requireToken => '"Bad rule"', ++ tokenUseGlobalStorage => 1, ++ } ++ } ++); ++ ++# Test normal first access ++# ------------------------ ++ok( $res = $client->_get( '/', accept => 'text/html' ), 'Unauth request' ); ++count(1); ++ ++my ( $host, $url, $query ) = expectForm( $res, '#', undef, 'token' ); ++ok( $query =~ /token=([^&]+)/, 'Token value' ); ++count(1); ++my $token = $1; ++$query =~ "token=$token"; ++ ++# Try to auth without token ++ok( ++ $res = $client->_post( ++ '/', ++ IO::String->new('user=dwho&password=dwho'), ++ length => 23 ++ ), ++ 'Try to auth without token' ++); ++count(1); ++expectReject($res); ++ ++# Try token as cookie value ++ok( $res = $client->_get( '/', cookie => "lemonldap=$token" ), ++ 'Try token as cookie' ); ++count(1); ++expectReject($res); ++ ++# Try to auth with token ++$query .= '&user=dwho&password=dwho'; ++ok( ++ $res = ++ $client->_post( '/', IO::String->new($query), length => length($query) ), ++ 'Try to auth with token' ++); ++count(1); ++expectOK($res); ++my $id = expectCookie($res); ++ ++# Verify auth ++ok( $res = $client->_get( '/', cookie => "lemonldap=$id" ), 'Verify auth' ); ++count(1); ++expectOK($res); ++ ++# Try to reuse the same token ++ok( ++ $res = ++ $client->_post( '/', IO::String->new($query), length => length($query) ), ++ 'Try to reuse the same token' ++); ++expectReject($res); ++ok( ++ $res = $client->_post( ++ '/', IO::String->new($query), ++ length => length($query), ++ accept => 'text/html' ++ ), ++ 'Verify that there is a new token' ++); ++expectForm( $res, '#', undef, 'token' ); ++count(2); ++ ++clean_sessions(); ++ ++done_testing( count() ); +--- /dev/null ++++ b/lemonldap-ng-portal/t/42-Register-Security.t +@@ -0,0 +1,78 @@ ++use Test::More; ++use strict; ++use IO::String; ++ ++BEGIN { ++ eval { ++ require 't/test-lib.pm'; ++ require 't/smtp.pm'; ++ }; ++} ++ ++my $maintests = 5; ++my ( $res, $user, $pwd ); ++ ++SKIP: { ++ eval 'require Email::Sender::Simple'; ++ if ($@) { ++ skip 'Missing dependencies', $maintests; ++ } ++ ++ my $client = LLNG::Manager::Test->new( { ++ ini => { ++ logLevel => 'error', ++ useSafeJail => 1, ++ portalDisplayRegister => 1, ++ authentication => 'Demo', ++ userDB => 'Same', ++ registerDB => 'Demo', ++ captcha_register_enabled => 0, ++ tokenUseGlobalStorage => 1, ++ } ++ } ++ ); ++ ++ # Test normal first access ++ # ------------------------ ++ ok( ++ $res = $client->_get( '/register', accept => 'text/html' ), ++ 'Unauth request', ++ ); ++ my ( $host, $url, $query ) = ++ expectForm( $res, '#', undef, 'firstname', 'lastname', 'mail' ); ++ ++ ok( ++ $res = $client->_post( ++ '/register', ++ IO::String->new( ++ 'firstname=fôo&lastname=bar&mail=foobar%40badwolf.org'), ++ length => 53, ++ accept => 'text/html' ++ ), ++ 'Ask to create account' ++ ); ++ expectOK($res); ++ ++ my $mail = mail(); ++ ok( $mail =~ m#a href="http://auth.example.com/register\?(.*?)"#, ++ 'Found register token' ); ++ $query = $1; ++ ok( $query =~ /register_token=([^&]+)/, 'Found register_token' ); ++ my $token = $1; ++ ++ ok( ++ $res = $client->_get( ++ '/', ++ length => 23, ++ cookie => "lemonldap=$token", ++ ), ++ 'Try to authenticate' ++ ); ++ expectReject($res); ++} ++count($maintests); ++ ++clean_sessions(); ++ ++done_testing( count() ); ++ +--- /dev/null ++++ b/lemonldap-ng-portal/t/77-2F-Mail-with-global-storage.t +@@ -0,0 +1,70 @@ ++use Test::More; ++use strict; ++use IO::String; ++use Data::Dumper; ++ ++require 't/test-lib.pm'; ++require 't/smtp.pm'; ++ ++use_ok('Lemonldap::NG::Common::FormEncode'); ++count(1); ++ ++my $client = LLNG::Manager::Test->new( { ++ ini => { ++ logLevel => 'error', ++ mail2fActivation => 1, ++ mail2fCodeRegex => '\d{4}', ++ authentication => 'Demo', ++ userDB => 'Same', ++ tokenUseGlobalStorage => 1, ++ } ++ } ++); ++ ++# Try to authenticate ++# ------------------- ++ok( ++ my $res = $client->_post( ++ '/', ++ IO::String->new('user=dwho&password=dwho'), ++ length => 23, ++ accept => 'text/html', ++ ), ++ 'Auth query' ++); ++count(1); ++ ++my ( $host, $url, $query ) = ++ expectForm( $res, undef, '/mail2fcheck', 'token', 'code' ); ++ ++ok( ++ $res->[2]->[0] =~ ++qr%<input name="code" value="" class="form-control" id="extcode" trplaceholder="code" autocomplete="off" />%, ++ 'Found EXTCODE input' ++) or print STDERR Dumper( $res->[2]->[0] ); ++count(1); ++ ++ok( mail() =~ m%<b>(\d{4})</b>%, 'Found 2F code in mail' ) ++ or print STDERR Dumper( mail() ); ++ ++my $code = $1; ++count(1); ++ ++$query =~ s/code=/code=${code}/; ++ok( ++ $res = $client->_post( ++ '/mail2fcheck', ++ IO::String->new($query), ++ length => length($query), ++ accept => 'text/html', ++ ), ++ 'Post code' ++); ++count(1); ++my $id = expectCookie($res); ++$client->logout($id); ++ ++clean_sessions(); ++ ++done_testing( count() ); ++ +--- a/lemonldap-ng-portal/MANIFEST ++++ b/lemonldap-ng-portal/MANIFEST +@@ -471,10 +471,12 @@ + t/40-Notifications-XML-Server.t + t/41-Captcha.t + t/41-Token.t ++t/41-Token-Global-Storage.t + t/42-Register-Demo-with-captcha.t + t/42-Register-Demo-with-token.t + t/42-Register-Demo.t + t/42-Register-LDAP.t ++t/42-Register-Security.t + t/43-MailPasswordReset-Choice.t + t/43-MailPasswordReset-DBI.t + t/43-MailPasswordReset-LDAP.t +@@ -511,6 +513,7 @@ + t/76-2F-Ext-with-BruteForce.t + t/76-2F-Ext-with-GrantSession.t + t/76-2F-Ext-with-HISTORY.t ++t/77-2F-Mail-with-global-storage.t + t/77-2F-Mail.t + t/90-Translations.t + t/99-pod.t diff --git a/debian/patches/series b/debian/patches/series index d6b7ea43b..cd6ff854e 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -3,3 +3,4 @@ Avoid-developer-tests.patch ignore-gpg-errors.diff fix-missing-userControl.diff update-translations.diff +CVE-2019-12046.patch