CVE-2021-41083 (published 2021-09-20T22:15Z)

Dada Mail is a web-based e-mail list management system. In affected versions a bad actor could give someone a carefully crafted web page via email, SMS, etc, that - when visited, allows them control of the list control panel as if the bad actor was logged in themselves. This includes changing any mailing list password, as well as the Dada Mail Root Password - which could effectively shut out actual list owners of the mailing list and allow the bad actor complete and unfettered control of your mailing list. This vulnerability also affects profile logins. For this vulnerability to work, the target of the bad actor would need to be logged into the list control panel themselves. This CSRF vulnerability in Dada Mail affects all versions of Dada Mail v11.15.1 and below. Although we know of no known CSRF exploits that have happened in the wild, this vulnerability has been confirmed by our testing, and by a third party. Users are advised to update to version 11.16.0.

Severity: MEDIUM
CVSS v2 base score: 6.8 (AV:N/AC:M/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-352
cwe_name: Cross-Site Request Forgery (CSRF)
description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
extended_description: When a web server is designed to receive a request from a client without any mechanism for verifying that it was intentionally sent, then it might be possible for an attacker to trick a client into making an unintentional request to the web server which will be treated as an authentic request. This can be done via a URL, image load, XMLHttpRequest, etc. and can result in exposure of data or unintended code execution.
         

Fix information:
repo_url: https://github.com/justingit/dada-mail
commit_details: repo_url: https://github.com/justingit/dada-mail
author: justingit
author_date: 2021-09-19 18:02:02-06:00
msg: This adds CSRF protection to the profiles.
repository: repo_url: https://github.com/justingit/dada-mail
repo_name: justingit/dada-mail
description: Self-Hosted, Full Featured, Email Mailing List Manager. Announcement + Discussion Lists, Web-based Installer, Installs with minimal dependencies, sendmail/SMTP/Amazon SES supported
repo_language: Perl
owner: justingit
file_changes: filename: App.pm
change_type: ModificationType.MODIFY
diff: @@ -15086,14 +15086,11 @@ sub profile_login {
         }
     }
 
-    if (   $DADA::Config::PROFILE_OPTIONS->{enabled} != 1) {
+    if ( $DADA::Config::PROFILE_OPTIONS->{enabled} != 1) {
         return $self->default();
 
     }
-
-    if ( $DADA::Config::PROFILE_OPTIONS->{enabled} != 1 ) {
-        return $self->default();
-    }
+	
     require DADA::Profile;
     ###
     my $all_errors = [];
@@ -15109,7 +15106,6 @@ sub profile_login {
     my $prof_sess = DADA::Profile::Session->new;
 
     if ( $q->param('process') != 1 ) {
-
         if (   $prof_sess->is_logged_in( { -cgi_obj => $q } )
             && $q->param('logged_out') != 1 )
         {
@@ -15118,13 +15114,20 @@ sub profile_login {
                 -url => $DADA::Config::PROGRAM_URL . '/profile/' );
         }
         else {
-            my $scrn              = '';
+			my $scrn              = '';
             my $using_captcha     = 0;
 
             if ( $DADA::Config::PROFILE_OPTIONS->{enable_captcha} == 1 ) {
                 $using_captcha = can_use_Google_reCAPTCHA();
             }
 
+			my $auth_state;
+		    if ( $DADA::Config::DISABLE_OUTSIDE_LOGINS == 1 ) {
+		        require DADA::Security::SimpleAuthStringState;
+		        my $sast = DADA::Security::SimpleAuthStringState->new;
+		        $auth_state = $sast->make_state;
+		    }
+
             $scrn = DADA::Template::Widgets::wrap_screen(
                 {
                     -screen => 'profile_login.tmpl',
@@ -15167,6 +15170,9 @@ sub profile_login {
                         removal         => scalar $q->param('removal') || '',
                         WHOLE_URL       => $whole_url,
 						
+		                auth_state      => $auth_state,
+						
+						
 						# This should probably be deprecated, as I'm handling this in 
 						# DADA::Template::Widgets, now
                         %{ DADA::Profile::feature_enabled() }
@@ -15180,8 +15186,9 @@ sub profile_login {
     else {
         my ( $status, $errors ) = $prof_sess->validate_profile_login(
             {
-                -email    => xss_filter( scalar $q->param('login_email') ),
-                -password => xss_filter( scalar $q->param('login_password') ),
+                -email      => xss_filter( scalar $q->param('login_email') ),
+                -password   => xss_filter( scalar $q->param('login_password') ),
+				-auth_state => xss_filter( scalar $q->param('auth_state') ),
 
             },
         );
@@ -15189,10 +15196,10 @@ sub profile_login {
         if ( $status == 1 ) {
             my $cookie = $prof_sess->login(
                 {
-                    -email => xss_filter( scalar $q->param('login_email') ),
-                    -password =>
-                      xss_filter( scalar $q->param('login_password') ),
-                },
+                    -email           => xss_filter( scalar $q->param('login_email') ),
+                    -password        => xss_filter( scalar $q->param('login_password') ),					
+                	-skip_validation => 1, 
+				},
             );
 
             #DEV: encoding?
@@ -15397,18 +15404,31 @@ sub profile {
     if (   $DADA::Config::PROFILE_OPTIONS->{enabled} != 1) {
         return $self->default();
     }
+	
+	
+	
 
     require DADA::Profile::Session;
     my $prof_sess = DADA::Profile::Session->new;
 
-    if ( $prof_sess->is_logged_in( { -cgi_obj => $q } ) ) {
-        my $email = $prof_sess->get( { -cgi_obj => $q } );
+    if ($prof_sess->is_logged_in( { -cgi_obj => $q } ) ) {
+		
+		if(length($q->param('process')) > 0) { 
+		
+			if($prof_sess->check_csrf($q) == 0){
+				$prof_sess->logout;
+				$q->param('flavor', 'profile_login');
+				return $self->profile_login();
+			}
+		}
+		
+        my $prof_data = $prof_sess->get( { -cgi_obj => $q } );
 
         require DADA::Profile::Fields;
         require DADA::Profile;
 
-        my $prof = DADA::Profile->new( { -email => $email } );
-        my $dpf = DADA::Profile::Fields->new( { -email => $email } );
+        my $prof = DADA::Profile->new( { -email => $prof_data->{email} } );
+        my $dpf = DADA::Profile::Fields->new( { -email => $prof_data->{email} } );
         my $subscriber_fields =
           $dpf->{manager}->fields( { -show_hidden_fields => 0, } );
         my $field_attr   = $dpf->{manager}->get_all_field_attributes;
@@ -15614,7 +15634,7 @@ sub profile {
             my $dps = DADA::Profile::Settings->new({-list => $list});
             my $r   = $dps->save(
                 {
-                    -email   => $email,
+                    -email   => $prof_data->{email},
                     -setting => 'delivery_prefs',
                     -value   => $delivery_prefs,
                 }
@@ -15682,7 +15702,7 @@ sub profile {
                 my $dasu = DADA::App::Subscriptions::Unsub->new(
                     { -list => $i->{list} } );
                 my $unsub_link = $dasu->unsub_link(
-                    { -email => $email, -mid => '00000000000000' } );
+                    { -email => $prof_data->{email}, -mid => '00000000000000' } );
 
                 my $digest_timeframe =
                   formatted_runtime( $ls->param('digest_schedule') );
@@ -15695,7 +15715,7 @@ sub profile {
 				);
                 my $s   = $dps->fetch(
                     {
-                        -email => $email,
+                        -email => $prof_data->{email},
                     }
                 );
                 my $delivery_prefs = $s->{delivery_prefs} || 'individual';
@@ -15722,7 +15742,8 @@ sub profile {
                     -vars   => {
                         errors => scalar $q->param('errors')
                           || 0,
-                        'profile.email'   => $email,
+                        'profile.email'   => $prof_data->{email},
+						csrf_token        => $prof_data->{token},
                         subscriber_fields => $fields,
                         subscriptions     => $filled,
                         has_subscriptions => $has_subscriptions,
@@ -15749,7 +15770,7 @@ sub profile {
                           $DADA::Config::PROFILE_OPTIONS->{gravatar_options}
                           ->{enable_gravators},
                         gravatar_img_url =>
-                          gravatar_img_url( { -email => $email, } ),
+                          gravatar_img_url( { -email => $prof_data->{email}, } ),
                         protected_directories => $protected_directories,
                         WHOLE_URL             => $whole_url,
                         %{ DADA::Profile::feature_enabled() },
@@ -16014,8 +16035,10 @@ sub profile_update_email {
             my $prof_sess = DADA::Profile::Session->new;
             my $cookie    = $prof_sess->login(
                 {
-                    -email   => $profile_info->{'profile.update_email'},
-                    -no_pass => 1,
+                    -email           => $profile_info->{'profile.update_email'},
+                    -no_pass         => 1,
+                	-skip_validation => 1, 
+					
                 }
             );
 

programming_language: Perl
filename: Session.pm
change_type: ModificationType.MODIFY
diff: @@ -90,13 +90,20 @@ sub _login_cookie {
 
     require CGI::Session;
 
+	use DADA::Security::Password; 
+
+	my $token = DADA::Security::Password::generate_rand_string(undef, 41); 
+
     my $session = new CGI::Session( $self->{dsn}, $q, $self->{dsn_args} );
 
-    $session->param( 'email',      cased($args->{ -email }) );
-    $session->param( '_logged_in', 1 );
+       $session->param( 'email',      cased($args->{ -email }) );
+       $session->param( '_logged_in', 1 );
+       $session->param( 'token',      $token);
+	
 
-    $session->expire( $DADA::Config::COOKIE_PARAMS{ -expires } );
-    $session->expire( '_logged_in', $DADA::Config::COOKIE_PARAMS{ -expires } );
+	    $session->expire( $DADA::Config::COOKIE_PARAMS{ -expires } );
+	    $session->expire( '_logged_in', $DADA::Config::COOKIE_PARAMS{ -expires } );
+	    $session->expire( 'token', $DADA::Config::COOKIE_PARAMS{ -expires } );
 
     $cookie = $q->cookie( 
 		%{$DADA::Config::PROFILE_OPTIONS->{cookie_params}},
@@ -106,10 +113,6 @@ sub _login_cookie {
 		) : ()
 		
 		);
-
-    # My proposal to address the situation is quit relying on flush() happen
-    # automatically, and recommend that people use an explicit flush()
-    # instead, which works reliably for everyone.
     $session->flush();
 
     return $cookie;
@@ -124,7 +127,16 @@ sub login {
 	require CGI; 
 	my $q = new CGI; 
 
-    my ( $status, $errors ) = $self->validate_profile_login($args);
+	my ($status, $errors);
+	
+	if($args->{-skip_validation} == 0){ 
+	    ( $status, $errors ) = $self->validate_profile_login($args);
+	}
+	else { 
+		$status = 1; 
+		$errors = {};
+	}
+	
     if ( $status == 0 ) {
         croak "login failed.";
     }
@@ -182,6 +194,20 @@ sub validate_profile_login {
         incorrect_pass => 0,
     };
 
+
+    require DADA::Security::SimpleAuthStringState;
+    my $sast       = DADA::Security::SimpleAuthStringState->new;
+    my $auth_state = $args->{-auth_state};
+
+    if ( $DADA::Config::DISABLE_OUTSIDE_LOGINS == 1 ) {
+        if ( $sast->check_state($auth_state) != 1 ) {
+	        $status = 0;
+	        $errors->{invalid_form} = 1;
+        }
+
+    }
+
+
     require DADA::Profile;
     my $prof = DADA::Profile->new(
 		{ 
@@ -217,6 +243,30 @@ sub validate_profile_login {
 
 }
 
+sub check_csrf { 
+	my $self = shift; 
+	my $q    = shift; 
+	
+    my $s = CGI::Session->load( $self->{dsn}, $q, $self->{dsn_args} )
+      or croak 'failed to load session: ' . CGI::Session->errstr();
+	
+      if ( $s->is_expired ) {
+          return 0;
+      }
+
+      if ( $s->is_empty ) {
+          return 0;
+      }
+	
+	  if($q->param('csrf_token') eq $s->param('token') ){ 
+		  return 1; 
+	  }
+	  else { 
+		  return 0; 
+	  }
+	
+
+}
 sub is_logged_in {
 
     my $self = shift;
@@ -230,6 +280,8 @@ sub is_logged_in {
         $q = new CGI;
 
     }
+	
+	
     my $s = CGI::Session->load( $self->{dsn}, $q, $self->{dsn_args} )
       or croak 'failed to load session: ' . CGI::Session->errstr();
 
@@ -272,7 +324,10 @@ sub get {
     require CGI::Session;
 
     my $session = new CGI::Session( $self->{dsn}, $q, $self->{dsn_args} );
-    return $session->param('email');
+    return {
+		email => $session->param('email'), 
+		token => $session->param('token'),
+	}; 
 
 }
 

programming_language: Perl
filename: Widgets.pm
change_type: ModificationType.MODIFY
diff: @@ -1250,7 +1250,7 @@ sub profile_widget {
 	
 	
     my $scr              = '';
-    my $email            = '';
+    my $prof_data        = {};
     my $is_logged_in     = 0;
     my $profiles_enabled = $DADA::Config::PROFILE_OPTIONS->{enabled};
     if (   $DADA::Config::PROFILE_OPTIONS->{enabled} != 1)
@@ -1261,7 +1261,7 @@ sub profile_widget {
 		if(defined($prof_sess_obj)){ 
 			if ( $prof_sess_obj->is_logged_in( { -cgi_obj => $q } ) ) {
                 $is_logged_in = 1;
-                $email = $prof_sess_obj->get( { -cgi_obj => $q } );
+                $prof_data = $prof_sess_obj->get( { -cgi_obj => $q } );
 			}
 		}
 		else {
@@ -1273,7 +1273,7 @@ sub profile_widget {
 	            my $prof_sess = DADA::Profile::Session->new;
 	            if ( $prof_sess->is_logged_in( { -cgi_obj => $q } ) ) {
 	                $is_logged_in = 1;
-	                $email = $prof_sess->get( { -cgi_obj => $q } );
+					$prof_data = $prof_sess->get( { -cgi_obj => $q } );
 	            }
 	        }
 		}
@@ -1285,11 +1285,11 @@ sub profile_widget {
             -vars   => {
                 profiles_enabled  => $profiles_enabled,
                 is_logged_in      => $is_logged_in,
-                'profile.email'   => $email,
+                'profile.email'   => $prof_data->{email},
                 gravators_enabled => $DADA::Config::PROFILE_OPTIONS->{gravatar_options}->{enable_gravators},
                 gravatar_img_url => gravatar_img_url(
                     {
-                        -email                => $email,
+                        -email                => $prof_data->{email},
                         -size => 45,
                     }
                 ),

programming_language: Perl
filename: profile_home.tmpl
change_type: ModificationType.MODIFY
diff: @@ -254,10 +254,11 @@
         <legend>Delivery Preferences:</legend>
         <form action="<!-- tmpl_var S_PROGRAM_URL -->" method="post"> 
 
-            <input type="hidden" name="flavor"        value="profile" > 
-            <input type="hidden" name="process"  value="profile_delivery_preferences" > 
-            <input type="hidden" name="list"     value="<!-- tmpl_var list_settings.list -->" > 
-
+            <input type="hidden" name="flavor"     value="profile" > 
+            <input type="hidden" name="process"    value="profile_delivery_preferences" > 
+            <input type="hidden" name="list"       value="<!-- tmpl_var list_settings.list -->" > 
+			<input type="hidden" name="csrf_token" value="<!-- tmpl_var csrf_token -->">
+			
 			<div class="row">
 				<div class="small-10 medium-5 large-4 columns">	 
 					
@@ -346,8 +347,10 @@
 							<div class="content-box section-box">
 
 				                <form action="<!-- tmpl_var PROGRAM_URL -->" method="post" accept-charset="<!-- tmpl_var HTML_CHARSET -->" id="membership_profile_fields"> 
-				                    <input type="hidden" name="flavor"        value="profile" > 
+				                    <input type="hidden" name="flavor"   value="profile" > 
 				                    <input type="hidden" name="process"  value="edit_subscriber_fields" > 
+									<input type="hidden" name="csrf_token" value="<!-- tmpl_var csrf_token -->">
+									
 				                    <!-- tmpl_loop subscriber_fields --> 
 
 										<div class="row">
@@ -425,6 +428,8 @@
 				            <form action="<!-- tmpl_var PROGRAM_URL -->" method="post" accept-charset="<!-- tmpl_var HTML_CHARSET -->"> 
 				                <input type="hidden" name="flavor"        value="profile" > 
 				                <input type="hidden" name="process"  value="update_email" >
+								<input type="hidden" name="csrf_token" value="<!-- tmpl_var csrf_token -->">
+								
 			                    <!-- tmpl_if errors_update_email --> 
 			                        <!-- tmpl_if error_invalid_email --> 
 			                            <p class="error"> 
@@ -471,6 +476,7 @@
 							<form action="<!-- tmpl_var PROGRAM_URL -->" method="post" accept-charset="<!-- tmpl_var HTML_CHARSET -->"> 
 				                <input type="hidden" name="flavor"        value="profile" > 
 				                <input type="hidden" name="process"  value="change_password" >
+								<input type="hidden" name="csrf_token" value="<!-- tmpl_var csrf_token -->">
 
 				                <!-- tmpl_if errors_change_password --> 
 				                        <p class="error">
@@ -513,6 +519,7 @@
 				            <form action="<!-- tmpl_var PROGRAM_URL -->" method="post" accept-charset="<!-- tmpl_var HTML_CHARSET -->"> 
 				                <input type="hidden" name="flavor"        value="profile" > 
 				                <input type="hidden" name="process"  value="delete_profile" >
+								<input type="hidden" name="csrf_token" value="<!-- tmpl_var csrf_token -->">
         
 								<div class="alert-box info radius">
 							        Deleting your profile will remove all your profile information, but 

programming_language: HTML
filename: profile_login.tmpl
change_type: ModificationType.MODIFY
diff: @@ -194,6 +194,15 @@
 						<input type="hidden" name="flavor" value="profile_login" > 
 						<input type="hidden" name="process" value="1" > 
 						
+						<!-- tmpl_if DISABLE_OUTSIDE_LOGINS --> 
+							<input 
+								type="hidden" 
+								name="auth_state" 
+								value="<!-- tmpl_var auth_state -->"
+							> 
+						<!--/tmpl_if--> 
+						
+						
 						<div class="row">
 							<div class="small-12 columns">
 								<label for="login_email">

programming_language: HTML