Bug#928954: unblock: lemonldap-ng/2.0.2+ds-7+deb10u1
Package: release.debian.org
Severity: normal
User: release.debian.org@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 <yadd@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 <yadd@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
Reply to: