Skip to content

Commit

Permalink
Fix missing CSRF mitigation
Browse files Browse the repository at this point in the history
  • Loading branch information
ehuelsmann committed Feb 2, 2024
1 parent d89c4d4 commit 8c2ae5b
Show file tree
Hide file tree
Showing 14 changed files with 99 additions and 5 deletions.
1 change: 1 addition & 0 deletions UI/setup/begin_backup.html
Expand Up @@ -9,6 +9,7 @@ <h2>[% text('Database Management Console') %]</h2>
[% # 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'
Expand Down
1 change: 1 addition & 0 deletions UI/setup/confirm_operation.html
Expand Up @@ -15,6 +15,7 @@ <h2>[% text('Database Management Console') %]</h2>
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'
Expand Down
1 change: 1 addition & 0 deletions UI/setup/credentials.html
Expand Up @@ -31,6 +31,7 @@ <h2 align="center" style="margin-top: 0">
<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 = {
Expand Down
2 changes: 2 additions & 0 deletions UI/setup/edit_user.html
Expand Up @@ -20,6 +20,7 @@
</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">
Expand Down Expand Up @@ -110,6 +111,7 @@
[% 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"
Expand Down
1 change: 1 addition & 0 deletions UI/setup/migration_step.html
Expand Up @@ -13,6 +13,7 @@ <h1 style="font-weight: bold; margin-bottom: 1em; text-align: center">
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 };
Expand Down
1 change: 1 addition & 0 deletions UI/setup/new_user.html
Expand Up @@ -9,6 +9,7 @@ <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'
Expand Down
1 change: 1 addition & 0 deletions UI/setup/select_coa.html
Expand Up @@ -7,6 +7,7 @@
<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>
Expand Down
1 change: 1 addition & 0 deletions UI/setup/template_info.html
Expand Up @@ -6,6 +6,7 @@
<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 = {
Expand Down
1 change: 1 addition & 0 deletions UI/setup/upgrade_info.html
Expand Up @@ -9,6 +9,7 @@ <h2>[% text('Database Management Console') %]</h2>
<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'
Expand Down
26 changes: 25 additions & 1 deletion lib/LedgerSMB.pm
Expand Up @@ -20,6 +20,16 @@ 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
Expand Down Expand Up @@ -246,7 +256,7 @@ 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;
Expand Down Expand Up @@ -309,6 +319,20 @@ sub new {
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',
Expand Down
6 changes: 4 additions & 2 deletions lib/LedgerSMB/Middleware/SessionStorage.pm
Expand Up @@ -33,6 +33,7 @@ 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;

Expand Down Expand Up @@ -62,8 +63,9 @@ sub call {
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 =
Expand Down
6 changes: 6 additions & 0 deletions lib/LedgerSMB/PSGI.pm
Expand Up @@ -355,6 +355,12 @@ sub setup_url_space {
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;
Expand Down
53 changes: 52 additions & 1 deletion lib/LedgerSMB/Scripts/setup.pm
Expand Up @@ -384,6 +384,9 @@ Copies db to the name of $request->{new_name}

sub copy_db {
my ($request) = @_;
if (my $csrf = $request->verify_csrf) {
return $csrf;
}
my ($reauth, $database) = _get_database($request);
return $reauth if $reauth;

Expand All @@ -402,6 +405,9 @@ Backs up a full db

sub backup_db {
my $request = shift @_;
if (my $csrf = $request->verify_csrf) {
return $csrf;
}
$request->{backup} = 'db';
return _begin_backup($request);
}
Expand All @@ -414,6 +420,9 @@ Backs up roles only (for all db's)

sub backup_roles {
my $request = shift @_;
if (my $csrf = $request->verify_csrf) {
return $csrf;
}
$request->{backup} = 'roles';
return _begin_backup($request);
}
Expand All @@ -439,6 +448,9 @@ Runs the backup. If backup_type is set to email, emails the

sub run_backup {
my $request = shift @_;
if (my $csrf = $request->verify_csrf) {
return $csrf;
}
my ($reauth, $database) = _get_database($request);
return $reauth if $reauth;

Expand Down Expand Up @@ -542,6 +554,9 @@ sub consistency {

sub revert_migration {
my ($request) = @_;
if (my $csrf = $request->verify_csrf) {
return $csrf;
}
my ($reauth, $database) = _get_database($request);
return $reauth if $reauth;

Expand Down Expand Up @@ -616,6 +631,9 @@ 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));
}
Expand Down Expand Up @@ -869,6 +887,9 @@ 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;

Expand Down Expand Up @@ -1038,6 +1059,9 @@ script.
sub fix_tests {
my ($request) = @_;

if (my $csrf = $request->verify_csrf) {
return $csrf;
}
my ($reauth, $database) = _init_db($request);
return $reauth if $reauth;

Expand Down Expand Up @@ -1093,6 +1117,9 @@ sub fix_tests {
sub create_db {
my ($request) = @_;

if (my $csrf = $request->verify_csrf) {
return $csrf;
}
my ($reauth, $database) = _get_database($request);
return $reauth if $reauth;

Expand Down Expand Up @@ -1153,6 +1180,9 @@ sub select_coa {
}

if ($request->{chart}) {
if (my $csrf = $request->verify_csrf) {
return $csrf;
}
my ($reauth, $database) = _get_database($request);
return $reauth if $reauth;

Expand Down Expand Up @@ -1400,6 +1430,9 @@ sub _create_initial_user {

sub add_user {
my ($request) = @_;
if (my $csrf = $request->verify_csrf) {
return $csrf;
}

return (_create_initial_user($request)
or login($request));
Expand Down Expand Up @@ -1455,6 +1488,9 @@ sub edit_user_roles {
sub save_user_roles {
my ($request) = @_;

if (my $csrf = $request->verify_csrf) {
return $csrf;
}
my ($reauth) = _init_db($request);
return $reauth if $reauth;

Expand Down Expand Up @@ -1495,6 +1531,9 @@ sub save_user_roles {
sub reset_password {
my ($request) = @_;

if (my $csrf = $request->verify_csrf) {
return $csrf;
}
my ($reauth) = _init_db($request);
return $reauth if $reauth;

Expand Down Expand Up @@ -1525,6 +1564,9 @@ Force work. Forgets unmatching tests, applies a curing statement and move on.

sub force {
my ($request) = @_;
if (my $csrf = $request->verify_csrf) {
return $csrf;
}
my ($reauth, $database) = _init_db($request);
return $reauth if $reauth;

Expand Down Expand Up @@ -1584,6 +1626,9 @@ 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;
}
Expand All @@ -1598,6 +1643,9 @@ Gets the statistics info and shows the complete screen.

sub _complete {
my ($request) = @_;
if (my $csrf = $request->verify_csrf) {
return $csrf;
}
my ($reauth, $database) = _init_db($request);
return $reauth if $reauth;

Expand All @@ -1617,6 +1665,9 @@ Asks the various modules for system and version info, showing the result

sub system_info {
my ($request) = @_;
if (my $csrf = $request->verify_csrf) {
return $csrf;
}
my ($reauth, $database) = _init_db($request);
return $reauth if $reauth;

Expand All @@ -1640,7 +1691,7 @@ sub system_info {
=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
Expand Down
3 changes: 2 additions & 1 deletion lib/LedgerSMB/Template/UI.pm
Expand Up @@ -135,7 +135,8 @@ sub render_string {
},
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,
Expand Down

0 comments on commit 8c2ae5b

Please sign in to comment.