CVE-2024-23831: Cross-Site Request Forgery (CSRF) (CWE-352)
value: LedgerSMB is a free web-based double-entry accounting system. When a LedgerSMB database administrator has an active session in /setup.pl, an attacker can trick the admin into clicking on a link which automatically submits a request to setup.pl without the admin's consent.  This request can be used to create a new user account with full application (/login.pl) privileges, leading to privilege escalation.  The vulnerability is patched in versions 1.10.30 and 1.11.9.

CWE 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.
CVSS v3 base score: 7.5
Published: 2024-02-02T16:15Z
Repository: https://github.com/ledgersmb/LedgerSMB
Language: HTML
Files changed: UI/setup/begin_backup.html, UI/setup/confirm_operation.html, UI/setup/credentials.html, UI/setup/edit_user.html, UI/setup/migration_step.html, UI/setup/new_user.html, UI/setup/select_coa.html, UI/setup/template_info.html, UI/setup/upgrade_info.html, lib/LedgerSMB.pm, lib/LedgerSMB/Middleware/SessionStorage.pm, lib/LedgerSMB/PSGI.pm, lib/LedgerSMB/Scripts/setup.pm, lib/LedgerSMB/Template/UI.pm
Commit date: 2024-02-02 12:56:29 +0100
Commit message: Fix missing CSRF mitigation

Diff: diff --git a/UI/setup/begin_backup.html b/UI/setup/begin_backup.html
index 2c1672946..f8a1286ff 100644
--- a/UI/setup/begin_backup.html
+++ b/UI/setup/begin_backup.html
@@ -7,10 +7,11 @@ include_stylesheet=["system/setup.css"] %]
 <h2>[% text('Database Management Console') %]</h2>
 <div class="listtop">[% text('Where shall we send the backup?') %]</div>
 [% # notice, message, and operation are all localized. %]
 <div id="notice">[% notice %]</div>
 <form data-dojo-type="lsmb/SimpleForm" action="setup.pl" method="POST" name="confirm_operation">
+  <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
 [% INCLUDE input element_data = {
     name = 'database'
     type = 'hidden'
    value = database
 } %]
diff --git a/UI/setup/confirm_operation.html b/UI/setup/confirm_operation.html
index b1d4883b9..c7ca18154 100644
--- a/UI/setup/confirm_operation.html
+++ b/UI/setup/confirm_operation.html
@@ -13,10 +13,11 @@ include_stylesheet=["system/setup.css"] %]
 <div id="operation">[% operation %]</div>
 <form data-dojo-type="lsmb/SimpleForm"
       action="setup.pl"
       method="POST"
       name="confirm_operation">
+  <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
 [% INCLUDE input element_data = {
     name = 'database'
     type = 'hidden'
    value = database
 } %]
diff --git a/UI/setup/credentials.html b/UI/setup/credentials.html
index 6f45785de..b7170e728 100644
--- a/UI/setup/credentials.html
+++ b/UI/setup/credentials.html
@@ -29,10 +29,11 @@
                     </div>
                 </div>
                 <form id="loginform"
                       name="credentials"
                       style="margin-top:1em">
+                  <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
                   <div class="login_form">
                     [% select_hint = text('Select or Enter User');
                        INCLUDE select element_data = {
                                    name = 's_user'
                                    id = 's-user'
diff --git a/UI/setup/edit_user.html b/UI/setup/edit_user.html
index 2bf373437..12d47dc8e 100644
--- a/UI/setup/edit_user.html
+++ b/UI/setup/edit_user.html
@@ -18,10 +18,11 @@ include_stylesheet=["system/setup.css"];
                       action = 'save_user';
                 END %]
         </div>
 
     <form data-dojo-type="lsmb/SimpleForm" method="POST" action="[% request.script %]">
+      <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
       <input type="hidden" name="id" value="[% request.id %]" />
       <input type="hidden" name="database" value="[% request.database %]" />
         <table id="user-data">
 
             [% IF user.user_id and not request.pls_import %]
@@ -108,10 +109,11 @@ include_stylesheet=["system/setup.css"];
     %]
     </form>
     [% IF user.user_id and not request.pls_import%]
         <hr />
         <form data-dojo-type="lsmb/SimpleForm" name="groups" method="POST" action="[% request.script %]">
+          <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
             <input type="hidden" name="database" value="[% request.database %]" />
             [% PROCESS input element_data = {
                type="hidden"
                name="id"
                value=request.id
diff --git a/UI/setup/migration_step.html b/UI/setup/migration_step.html
index e6306c6e8..af2e19468 100644
--- a/UI/setup/migration_step.html
+++ b/UI/setup/migration_step.html
@@ -11,10 +11,11 @@
     [% # We cannot use form-dynatable, for it requires lsmb/Form and maindiv %]
     <form data-dojo-type="lsmb/SimpleForm"
           method="post"
           action="[% form.script %]"
           id="migration-step-dynatable">
+      <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
     [% FOREACH header IN headers %]<div class="listtop">
              [% INCLUDE decorated_text element_data = {
                     msg => header };
              %]</div>
     [% END %]
diff --git a/UI/setup/new_user.html b/UI/setup/new_user.html
index c0efb3656..2b921c7b9 100644
--- a/UI/setup/new_user.html
+++ b/UI/setup/new_user.html
@@ -7,10 +7,11 @@ include_stylesheet=["system/setup.css"] %]
     <div class="setupconsole">
       <h2>[% text('Database Management Console') %]</h2>
       <div class="listtop">[% text('Enter User') %]</div>
       <form data-dojo-type="lsmb/SimpleForm"
             action="setup.pl" method="POST" name="new_user">
+        <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
         [% INCLUDE input element_data = {
                       name = 'database'
                       type = 'hidden'
                       value = database
         } %]
diff --git a/UI/setup/select_coa.html b/UI/setup/select_coa.html
index 73689e6ef..b0b8ebafa 100644
--- a/UI/setup/select_coa.html
+++ b/UI/setup/select_coa.html
@@ -5,10 +5,11 @@ include_stylesheet=["system/setup.css"] %]
 <body id="setup-select-coa" class="lsmb [% dojo_theme %]">
 <div class="setupconsole">
 <h2>[% text('Database Management Console') %]</h2>
 <div><div class="listtop">[% title %]</div>
 <form data-dojo-type="lsmb/SimpleForm" action="setup.pl" method="POST" name="credentials">
+  <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
   <div id="sep" class="listheading">
     Pre-defined Chart-of-Accounts selection
   </div>
 
   [% IF coa_lc;
diff --git a/UI/setup/template_info.html b/UI/setup/template_info.html
index 66df5db8c..078511b64 100644
--- a/UI/setup/template_info.html
+++ b/UI/setup/template_info.html
@@ -4,10 +4,11 @@
     PROCESS "elements.html"
 %]<body id="setup-template-info" class="lsmb [% dojo_theme %]">
 <div><div class="setupconsole">
 <h2>[% text('Database Management Console') %]</h2>
 <form data-dojo-type="lsmb/SimpleForm" action="setup.pl" method="post">
+  <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
 <div class="listtop">[% text('Select Templates to Load') %]</div>
 [%
 PROCESS input element_data = {
     type = "hidden"
     name = "database"
diff --git a/UI/setup/upgrade_info.html b/UI/setup/upgrade_info.html
index 0075ee40c..fa1a134eb 100644
--- a/UI/setup/upgrade_info.html
+++ b/UI/setup/upgrade_info.html
@@ -7,10 +7,11 @@ include_stylesheet=["system/setup.css"] %]
 <h2>[% text('Database Management Console') %]</h2>
 <div class="listtop">[% text('Upgrade Info') %]</div>
 <form data-dojo-type="lsmb/SimpleForm"
       action="setup.pl" method="POST"
       name="upgrade_info">
+  <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
 [% INCLUDE input element_data = {
     name = 'database'
     type = 'hidden'
    value = database
 };
diff --git a/lib/LedgerSMB.pm b/lib/LedgerSMB.pm
index 989041faa..1ed105b46 100644
--- a/lib/LedgerSMB.pm
+++ b/lib/LedgerSMB.pm
@@ -18,10 +18,20 @@ in database objects (LedgerSMB::DBObject)
 
 This method creates a new base request instance. It also validates the
 session/user credentials, as appropriate for the run mode.  Finally, it sets up
 the database connections for the user.
 
+=item verify_csrf()
+
+This method verifies the C<csrf_token> value in request parameters (held in
+C<$self->{csrf_token}>) against the value in the session object.  When one is
+not defined or they are not equal, this function returns a PSGI triplet to be
+used as the response resulting in a 400 -- Bad Request.
+
+When the CSRF token matches, C<undef> is returned indicating processing is to
+continue.
+
 =item open_form()
 
 This sets a $self->{form_id} to be used in later form validation (anti-XSRF
 measure).
 
@@ -244,11 +254,11 @@ use warnings;
 
 use Carp;
 use DateTime::Format::Duration::ISO8601;
 use Encode qw(perlio_ok);
 use HTTP::Headers::Fast;
-use HTTP::Status qw( HTTP_OK );
+use HTTP::Status qw( HTTP_OK HTTP_BAD_REQUEST );
 use List::Util qw( pairgrep );
 use Locale::CLDR;
 use Locales unicode => 1;
 use Log::Any;
 use PGObject;
@@ -307,10 +317,24 @@ sub new {
     $self->_set_default_locale();
 
     return $self;
 }
 
+
+sub verify_csrf {
+    my ($self) = @_;
+    my $got = $self->{csrf_token};
+    my $want = $self->{_req}->env->{'lsmb.session'}->{csrf_token};
+    if (not ($got and $want and $got eq $want)) {
+        $logger->debug( "CSRF have '$got'; want '$want'" );
+        return [ HTTP_BAD_REQUEST,
+                 [ 'Content-Type' => 'text/plain; charset=ascii' ],
+                 [ 'Bad request: CSRF token failure' ] ];
+    }
+    return undef;
+}
+
 sub open_form {
     my ($self) = @_;
     my @vars = $self->call_procedure(procname => 'form_open',
                               args => [$self->{_session_id}],
                               continue_on_error => 1
diff --git a/lib/LedgerSMB/Middleware/SessionStorage.pm b/lib/LedgerSMB/Middleware/SessionStorage.pm
index 93b6c6fae..ba2eba3ac 100644
--- a/lib/LedgerSMB/Middleware/SessionStorage.pm
+++ b/lib/LedgerSMB/Middleware/SessionStorage.pm
@@ -31,10 +31,11 @@ use Cookie::Baker;
 use Plack::Request;
 use Plack::Util;
 use Plack::Util::Accessor
     qw( cookie cookie_path domain duration inner_serialize secret store );
 use Session::Storage::Secure;
+use String::Random;
 
 use LedgerSMB::PSGI::Util;
 
 =head1 METHODS
 
@@ -60,12 +61,13 @@ sub prepare_app {
 sub call {
     my $self = shift;
     my ($env) = @_;
     my $req = Plack::Request->new($env);
 
-    my $cookie      = $req->cookies->{$self->cookie};
-    my $session     = $self->store->decode($cookie);
+    my $cookie             = $req->cookies->{$self->cookie};
+    my $session            = $self->store->decode($cookie);
+    $session->{csrf_token} //= String::Random->new->randpattern('.' x 23);
 
     my $secure = defined($env->{HTTPS}) && $env->{HTTPS} eq 'ON';
     my $path =
         $self->cookie_path //
         LedgerSMB::PSGI::Util::cookie_path($env->{SCRIPT_NAME});
diff --git a/lib/LedgerSMB/PSGI.pm b/lib/LedgerSMB/PSGI.pm
index ca688e007..f038e0046 100644
--- a/lib/LedgerSMB/PSGI.pm
+++ b/lib/LedgerSMB/PSGI.pm
@@ -353,10 +353,16 @@ sub setup_url_space {
             enable '+LedgerSMB::Middleware::DynamicLoadWorkflow',
                 max_post_size => $wire->get( 'miscellaneous/max_upload_size' ),
                 script => 'setup.pl';
             enable '+LedgerSMB::Middleware::Log4perl',
                 script => 'setup.pl';
+            enable '+LedgerSMB::Middleware::SessionStorage',
+                domain      => 'setup',
+                cookie      => "$cookie~setup",
+                cookie_path => '/',
+                secret      => $secret,
+                duration    => 60*60*24*90;
             enable '+LedgerSMB::Middleware::SetupAuthentication';
             enable '+LedgerSMB::Middleware::DisableBackButton';
             $psgi_app;
         };
 
diff --git a/lib/LedgerSMB/Scripts/setup.pm b/lib/LedgerSMB/Scripts/setup.pm
index cacdb87b8..daa07c4c6 100644
--- a/lib/LedgerSMB/Scripts/setup.pm
+++ b/lib/LedgerSMB/Scripts/setup.pm
@@ -382,10 +382,13 @@ Copies db to the name of $request->{new_name}
 
 =cut
 
 sub copy_db {
     my ($request) = @_;
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     my ($reauth, $database) = _get_database($request);
     return $reauth if $reauth;
 
     $database->copy($request->{new_name})
            || die 'An error occurred. Please check your database logs.' ;
@@ -400,10 +403,13 @@ Backs up a full db
 
 =cut
 
 sub backup_db {
     my $request = shift @_;
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     $request->{backup} = 'db';
     return _begin_backup($request);
 }
 
 =item backup_roles
@@ -412,10 +418,13 @@ Backs up roles only (for all db's)
 
 =cut
 
 sub backup_roles {
     my $request = shift @_;
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     $request->{backup} = 'roles';
     return _begin_backup($request);
 }
 
 # Private method, basically just passes the inputs on to the next screen.
@@ -437,10 +446,13 @@ Runs the backup.  If backup_type is set to email, emails the
 
 =cut
 
 sub run_backup {
     my $request = shift @_;
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     my ($reauth, $database) = _get_database($request);
     return $reauth if $reauth;
 
     my $backuptype;
     my $backupfile;
@@ -540,10 +552,13 @@ sub consistency {
 
 =cut
 
 sub revert_migration {
     my ($request) = @_;
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     my ($reauth, $database) = _get_database($request);
     return $reauth if $reauth;
 
     my $dbh = $database->connect({PrintError => 0, AutoCommit => 0});
     my $sth = $dbh->prepare(q(
@@ -614,10 +629,13 @@ sub _save_templates {
 }
 
 sub load_templates {
     my ($request) = @_;
 
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     return (_save_templates($request, 'load_templates')
             or login($request));
 }
 
 
@@ -867,10 +885,13 @@ sub _load_templates {
 
 
 
 sub upgrade {
     my ($request) = @_;
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     my ($reauth, $database) = _init_db($request);
     return $reauth if $reauth;
 
     my $settings = $request->{_wire}->get( 'setup_settings' );
     my $auth_db = ($settings and $settings->{auth_db}) // 'postgres';
@@ -1036,10 +1057,13 @@ script.
 =cut
 
 sub fix_tests {
     my ($request) = @_;
 
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     my ($reauth, $database) = _init_db($request);
     return $reauth if $reauth;
 
     my $settings = $request->{_wire}->get( 'setup_settings' );
     my $auth_db = ($settings and $settings->{auth_db}) // 'postgres';
@@ -1091,10 +1115,13 @@ sub fix_tests {
 =cut
 
 sub create_db {
     my ($request) = @_;
 
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     my ($reauth, $database) = _get_database($request);
     return $reauth if $reauth;
 
     my $settings = $request->{_wire}->get( 'setup_settings' );
     my $auth_db = ($settings and $settings->{auth_db}) // 'postgres';
@@ -1151,10 +1178,13 @@ sub select_coa {
                 }
             }
         }
 
         if ($request->{chart}) {
+            if (my $csrf = $request->verify_csrf) {
+                return $csrf;
+            }
             my ($reauth, $database) = _get_database($request);
             return $reauth if $reauth;
 
             my $c = LedgerSMB::Company->new(
                 dbh => $database->connect(),
@@ -1398,10 +1428,13 @@ sub _create_initial_user {
             or _dispatch_upgrade_workflow($request, '_create_initial_user'));
 }
 
 sub add_user {
     my ($request) = @_;
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
 
     return (_create_initial_user($request)
             or login($request));
 }
 
@@ -1453,10 +1486,13 @@ sub edit_user_roles {
 =cut
 
 sub save_user_roles {
     my ($request) = @_;
 
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     my ($reauth) = _init_db($request);
     return $reauth if $reauth;
 
     my ($user) = $request->call_procedure(
         funcname => 'admin__get_user',
@@ -1493,10 +1529,13 @@ sub save_user_roles {
 =cut
 
 sub reset_password {
     my ($request) = @_;
 
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     my ($reauth) = _init_db($request);
     return $reauth if $reauth;
 
     my $user = LedgerSMB::Entity::User->new(%$request);
     $user->reset_password($request->{password});
@@ -1523,10 +1562,13 @@ Force work.  Forgets unmatching tests, applies a curing statement and move on.
 
 =cut
 
 sub force {
     my ($request) = @_;
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     my ($reauth, $database) = _init_db($request);
     return $reauth if $reauth;
 
     my $hdr = $request->{_req}->header( 'Accept-Language' );
     my $lang = $request->{_wire}->get( 'default_locale' )->from_header( $hdr );
@@ -1582,10 +1624,13 @@ sub _rebuild_modules {
 }
 
 sub rebuild_modules {
     my ($request) = @_;
 
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     if (my $rv = _rebuild_modules($request, 'rebuild_modules')) {
         return $rv;
     }
     return complete($request);
 }
@@ -1596,10 +1641,13 @@ Gets the statistics info and shows the complete screen.
 
 =cut
 
 sub _complete {
     my ($request) = @_;
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     my ($reauth, $database) = _init_db($request);
     return $reauth if $reauth;
 
     my $temp = $database->loader_log_filename();
     $request->{lsmb_info} = $database->stats();
@@ -1615,10 +1663,13 @@ Asks the various modules for system and version info, showing the result
 
 =cut
 
 sub system_info {
     my ($request) = @_;
+    if (my $csrf = $request->verify_csrf) {
+        return $csrf;
+    }
     my ($reauth, $database) = _init_db($request);
     return $reauth if $reauth;
 
     # the intent here is to get a much more sophisticated system which
     # asks registered modules for their system and dependency info
@@ -1638,11 +1689,11 @@ sub system_info {
 
 =back
 
 =head1 LICENSE AND COPYRIGHT
 
-Copyright (C) 2011-2022 The LedgerSMB Core Team
+Copyright (C) 2011-2024 The LedgerSMB Core Team
 
 This file is licensed under the GNU General Public License version 2, or at your
 option any later version.  A copy of the license should have been included with
 your software.
 
diff --git a/lib/LedgerSMB/Template/UI.pm b/lib/LedgerSMB/Template/UI.pm
index c20165439..de8602a97 100644
--- a/lib/LedgerSMB/Template/UI.pm
+++ b/lib/LedgerSMB/Template/UI.pm
@@ -133,11 +133,12 @@ sub render_string {
                         # Reports rendered as UI elements have SETTINGS in $vars
                         ($vars->{SETTINGS} // {})->%*
                     },
                     dojo_theme => (
                         $request->{_company_config}->{dojo_theme} || 'claro'
-                        )
+                        ),
+                    csrf_token => $request->{_req}->env->{'lsmb.session'}->{csrf_token},
                 },
                 sub { return escape_html($_[0]); },
                 $request->formatter_options,
                 )},
           %{$self->{standard_vars}},

Vulnerable code: use HTTP::Status qw( HTTP_OK );
    my $cookie      = $req->cookies->{$self->cookie};
    my $session     = $self->store->decode($cookie);
Copyright (C) 2011-2022 The LedgerSMB Core Team
                        )
Fixed code:   <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
  <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
                  <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
      <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
          <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
      <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
        <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
  <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
  <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
  <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
=item verify_csrf()

This method verifies the C<csrf_token> value in request parameters (held in
C<$self->{csrf_token}>) against the value in the session object.  When one is
not defined or they are not equal, this function returns a PSGI triplet to be
used as the response resulting in a 400 -- Bad Request.

When the CSRF token matches, C<undef> is returned indicating processing is to
continue.

use HTTP::Status qw( HTTP_OK HTTP_BAD_REQUEST );

sub verify_csrf {
    my ($self) = @_;
    my $got = $self->{csrf_token};
    my $want = $self->{_req}->env->{'lsmb.session'}->{csrf_token};
    if (not ($got and $want and $got eq $want)) {
        $logger->debug( "CSRF have '$got'; want '$want'" );
        return [ HTTP_BAD_REQUEST,
                 [ 'Content-Type' => 'text/plain; charset=ascii' ],
                 [ 'Bad request: CSRF token failure' ] ];
    }
    return undef;
}

use String::Random;
    my $cookie             = $req->cookies->{$self->cookie};
    my $session            = $self->store->decode($cookie);
    $session->{csrf_token} //= String::Random->new->randpattern('.' x 23);
            enable '+LedgerSMB::Middleware::SessionStorage',
                domain      => 'setup',
                cookie      => "$cookie~setup",
                cookie_path => '/',
                secret      => $secret,
                duration    => 60*60*24*90;
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
            if (my $csrf = $request->verify_csrf) {
                return $csrf;
            }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
    if (my $csrf = $request->verify_csrf) {
        return $csrf;
    }
Copyright (C) 2011-2024 The LedgerSMB Core Team
                        ),
                    csrf_token => $request->{_req}->env->{'lsmb.session'}->{csrf_token},