Bug 36598: Enable CSRF protection for Mojolicious apps
Test plan: 1. Run bin/opac daemon -l http://*:3001/ 2. Go to http://localhost:3001/cgi-bin/koha/opac-user.pl 3. With browser devtools, locate csrf_token hidden input within the login form and remove it or modify it 4. Try to submit the form with correct credentials, it should fail ("Wrong CSRF token") 5. Reload the page, try to log in normally without modifying the DOM, it should succeed 6. Run bin/intranet daemon -l http://*:3002/ 7. Go to http://localhost:3002/cgi-bin/koha/mainpage.pl 8. With browser devtools, locate csrf_token hidden input within the login form and remove it or modify it 9. Try to submit the form with correct credentials, it should fail ("Wrong CSRF token") 10. Reload the page, try to log in normally without modifying the DOM, it should succeed 11. Run prove t/db_dependent/mojo/csrf.t Signed-off-by: Matt Blenkinsop <matt.blenkinsop@ptfs-europe.com> Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net> Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
parent
ae84680c4d
commit
84054b932c
|
|
@ -38,6 +38,8 @@ sub startup {
|
|||
# FIXME This generates routes like this: /api/api/v1/...
|
||||
$self->plugin('RESTV1');
|
||||
|
||||
$self->plugin('CSRF');
|
||||
|
||||
$self->hook(before_dispatch => \&_before_dispatch);
|
||||
$self->hook(around_action => \&_around_action);
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ sub startup {
|
|||
# FIXME This generates routes like this: /api/api/v1/...
|
||||
$self->plugin('RESTV1');
|
||||
|
||||
$self->plugin('CSRF');
|
||||
|
||||
$self->hook(before_dispatch => \&_before_dispatch);
|
||||
$self->hook(around_action => \&_around_action);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
package Koha::App::Plugin::CSRF;
|
||||
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Koha; if not, see <http://www.gnu.org/licenses>.
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Koha::App::Plugin::CSRF
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
$app->plugin('CSRF');
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Enables CSRF protection in a Mojolicious app
|
||||
|
||||
=cut
|
||||
|
||||
use Modern::Perl;
|
||||
|
||||
use Mojo::Base 'Mojolicious::Plugin';
|
||||
|
||||
use Mojo::Message::Response;
|
||||
|
||||
use Koha::Token;
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=head2 register
|
||||
|
||||
Called by Mojolicious when the plugin is loaded.
|
||||
|
||||
Defines an `around_action` hook that will return a 403 response if CSRF token
|
||||
is missing or invalid.
|
||||
|
||||
This verification occurs only for HTTP methods POST, PUT, DELETE and PATCH.
|
||||
|
||||
If CGISESSID cookie is missing, it means that we are not authenticated or we
|
||||
are authenticated to the API by another method (HTTP basic or OAuth2). In this
|
||||
case, no verification is done.
|
||||
|
||||
=cut
|
||||
|
||||
sub register {
|
||||
my ( $self, $app, $conf ) = @_;
|
||||
|
||||
$app->hook(
|
||||
around_action => sub {
|
||||
my ( $next, $c, $action, $last ) = @_;
|
||||
|
||||
my $method = $c->req->method;
|
||||
if ( $method eq 'POST' || $method eq 'PUT' || $method eq 'DELETE' || $method eq 'PATCH' ) {
|
||||
if ( $c->cookie('CGISESSID') && !$self->is_csrf_valid( $c->req ) ) {
|
||||
return $c->reply->exception('Wrong CSRF token')->rendered(403);
|
||||
}
|
||||
}
|
||||
|
||||
return $next->();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
=head2 is_csrf_valid
|
||||
|
||||
Checks if a CSRF token exists and is valid
|
||||
|
||||
$is_valid = $plugin->is_csrf_valid($req)
|
||||
|
||||
C<$req> must be a Mojo::Message::Request object
|
||||
|
||||
=cut
|
||||
|
||||
sub is_csrf_valid {
|
||||
my ( $self, $req ) = @_;
|
||||
|
||||
my $csrf_token = $req->param('csrf_token') || $req->headers->header('CSRF_TOKEN');
|
||||
my $cookie = $req->cookie('CGISESSID');
|
||||
if ( $csrf_token && $cookie ) {
|
||||
my $session_id = $cookie->value;
|
||||
|
||||
return Koha::Token->new->check_csrf( { session_id => $session_id, token => $csrf_token } );
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
#!/usr/bin/perl
|
||||
|
||||
use Modern::Perl;
|
||||
|
||||
use Test::More tests => 2;
|
||||
use Test::Mojo;
|
||||
|
||||
use Koha::Database;
|
||||
|
||||
subtest 'CSRF - Intranet' => sub {
|
||||
plan tests => 4;
|
||||
|
||||
my $t = Test::Mojo->new('Koha::App::Intranet');
|
||||
|
||||
# Make sure we have a CGISESSID cookie
|
||||
$t->get_ok('/cgi-bin/koha/mainpage.pl');
|
||||
|
||||
subtest 'Without a CSRF token' => sub {
|
||||
plan tests => 3;
|
||||
|
||||
$t->post_ok('/cgi-bin/koha/mainpage.pl')->status_is(403)
|
||||
->content_like( qr/Wrong CSRF token/, 'Body contains "Wrong CSRF token"' );
|
||||
};
|
||||
|
||||
subtest 'With a wrong CSRF token' => sub {
|
||||
plan tests => 3;
|
||||
|
||||
$t->post_ok( '/cgi-bin/koha/mainpage.pl', form => { csrf_token => 'BAD', op => 'cud-login' } )->status_is(403)
|
||||
->content_like( qr/Wrong CSRF token/, 'Body contains "Wrong CSRF token"' );
|
||||
};
|
||||
|
||||
subtest 'With a good CSRF token' => sub {
|
||||
plan tests => 4;
|
||||
|
||||
$t->get_ok('/cgi-bin/koha/mainpage.pl');
|
||||
|
||||
my $csrf_token = $t->tx->res->dom('input[name="csrf_token"]')->map( attr => 'value' )->first;
|
||||
|
||||
$t->post_ok( '/cgi-bin/koha/mainpage.pl', form => { csrf_token => $csrf_token, op => 'cud-login' } )
|
||||
->status_is(200)->content_like( qr/Please log in again/, 'Login failed but CSRF test passed' );
|
||||
};
|
||||
};
|
||||
|
||||
subtest 'CSRF - OPAC' => sub {
|
||||
plan tests => 4;
|
||||
|
||||
my $t = Test::Mojo->new('Koha::App::Opac');
|
||||
|
||||
# Make sure we have a CGISESSID cookie
|
||||
$t->get_ok('/cgi-bin/koha/opac-user.pl');
|
||||
|
||||
subtest 'Without a CSRF token' => sub {
|
||||
plan tests => 3;
|
||||
|
||||
$t->post_ok('/cgi-bin/koha/opac-user.pl')->status_is(403)
|
||||
->content_like( qr/Wrong CSRF token/, 'Body contains "Wrong CSRF token"' );
|
||||
};
|
||||
|
||||
subtest 'With a wrong CSRF token' => sub {
|
||||
plan tests => 3;
|
||||
|
||||
$t->post_ok( '/cgi-bin/koha/opac-user.pl', form => { csrf_token => 'BAD', op => 'cud-login' } )->status_is(403)
|
||||
->content_like( qr/Wrong CSRF token/, 'Body contains "Wrong CSRF token"' );
|
||||
};
|
||||
|
||||
subtest 'With a good CSRF token' => sub {
|
||||
plan tests => 4;
|
||||
|
||||
$t->get_ok('/cgi-bin/koha/opac-user.pl');
|
||||
|
||||
my $csrf_token = $t->tx->res->dom('input[name="csrf_token"]')->map( attr => 'value' )->first;
|
||||
|
||||
$t->post_ok( '/cgi-bin/koha/opac-user.pl', form => { csrf_token => $csrf_token, op => 'cud-login' } )
|
||||
->status_is(200)->content_like( qr/Log in to your account/, 'Login failed but CSRF test passed' );
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue