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

Reply via email to